Skip to content

Commit 0fbfd74

Browse files
committed
[LifetimeSafety] Add script performance benchmarking
1 parent e870b04 commit 0fbfd74

File tree

2 files changed

+221
-1
lines changed

2 files changed

+221
-1
lines changed

clang/lib/Analysis/LifetimeSafety.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,12 @@ class OriginManager {
151151

152152
OriginID get(const ValueDecl &D) {
153153
auto It = DeclToOriginID.find(&D);
154-
assert(It != DeclToOriginID.end());
154+
// TODO: This should be an assert(It != ExprToOriginID.end()). The current
155+
// implementation falls back to getOrCreate to avoid crashing on
156+
// yet-unhandled pointer expressions, creating an empty origin for them.
157+
if (It == DeclToOriginID.end())
158+
return getOrCreate(D);
159+
155160
return It->second;
156161
}
157162

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import sys
2+
import argparse
3+
import subprocess
4+
import tempfile
5+
import json
6+
import os
7+
from datetime import datetime
8+
import numpy as np
9+
from scipy.optimize import curve_fit
10+
from scipy.stats import t
11+
12+
def generate_cpp_cycle_test(n: int) -> str:
13+
"""
14+
Generates a C++ code snippet with a specified number of pointers in a cycle.
15+
"""
16+
if n <= 0:
17+
return "// Number of variables must be positive."
18+
19+
cpp_code = "struct MyObj { int id; ~MyObj() {} };\n\n"
20+
cpp_code += f"void long_cycle_{n}(bool condition) {{\n"
21+
for i in range(1, n + 1):
22+
cpp_code += f" MyObj v{i}{{1}};\n"
23+
cpp_code += "\n"
24+
for i in range(1, n + 1):
25+
cpp_code += f" MyObj* p{i} = &v{i};\n"
26+
27+
cpp_code += "\n while (condition) {\n"
28+
if n > 0:
29+
cpp_code += f" MyObj* temp = p1;\n"
30+
for i in range(1, n):
31+
cpp_code += f" p{i} = p{i+1};\n"
32+
cpp_code += f" p{n} = temp;\n"
33+
cpp_code += " }\n}\n"
34+
cpp_code += f"\nint main() {{ long_cycle_{n}(false); return 0; }}\n"
35+
return cpp_code
36+
37+
def generate_cpp_merge_test(n: int) -> str:
38+
"""
39+
Generates a C++ code snippet with N independent conditional assignments.
40+
"""
41+
if n <= 0:
42+
return "// Number of variables must be positive."
43+
44+
cpp_code = "struct MyObj { int id; ~MyObj() {} };\n\n"
45+
cpp_code += f"void conditional_merges_{n}(bool condition) {{\n"
46+
decls = [f"v{i}" for i in range(1, n + 1)]
47+
cpp_code += f" MyObj {', '.join(decls)};\n"
48+
ptr_decls = [f"*p{i} = nullptr" for i in range(1, n + 1)]
49+
cpp_code += f" MyObj {', '.join(ptr_decls)};\n\n"
50+
51+
for i in range(1, n + 1):
52+
cpp_code += f" if(condition) {{ p{i} = &v{i}; }}\n"
53+
54+
cpp_code += "}\n"
55+
cpp_code += f"\nint main() {{ conditional_merges_{n}(false); return 0; }}\n"
56+
return cpp_code
57+
58+
def analyze_trace_file(trace_path: str) -> tuple[float, float]:
59+
"""
60+
Parses the -ftime-trace JSON output to find durations.
61+
62+
Returns:
63+
A tuple of (lifetime_analysis_duration_us, total_clang_duration_us).
64+
"""
65+
lifetime_duration = 0.0
66+
total_duration = 0.0
67+
try:
68+
with open(trace_path, 'r') as f:
69+
trace_data = json.load(f)
70+
for event in trace_data.get('traceEvents', []):
71+
if event.get('name') == 'LifetimeAnalysis':
72+
lifetime_duration += float(event.get('dur', 0))
73+
if event.get('name') == 'ExecuteCompiler':
74+
total_duration += float(event.get('dur', 0))
75+
76+
except (IOError, json.JSONDecodeError) as e:
77+
print(f"Error reading or parsing trace file {trace_path}: {e}", file=sys.stderr)
78+
return 0.0, 0.0
79+
return lifetime_duration, total_duration
80+
81+
def power_law(n, c, k):
82+
"""Represents the power law function: y = c * n^k"""
83+
return c * np.power(n, k)
84+
85+
def human_readable_time(ms: float) -> str:
86+
"""Converts milliseconds to a human-readable string (ms or s)."""
87+
if ms >= 1000:
88+
return f"{ms / 1000:.2f} s"
89+
return f"{ms:.2f} ms"
90+
91+
def generate_markdown_report(results: dict) -> str:
92+
"""Generates a Markdown-formatted report from the benchmark results."""
93+
report = []
94+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")
95+
report.append(f"# Lifetime Analysis Performance Report")
96+
report.append(f"> Generated on: {timestamp}")
97+
report.append("\n---\n")
98+
99+
for test_type, data in results.items():
100+
title = 'Pointer Cycle in Loop' if test_type == 'cycle' else 'CFG Merges'
101+
report.append(f"## Test Case: {title}")
102+
report.append("")
103+
104+
# Table header
105+
report.append("| N | Analysis Time | Total Clang Time |")
106+
report.append("|:----|--------------:|-----------------:|")
107+
108+
# Table rows
109+
n_data = np.array(data['n'])
110+
analysis_data = np.array(data['lifetime_ms'])
111+
total_data = np.array(data['total_ms'])
112+
for i in range(len(n_data)):
113+
analysis_str = human_readable_time(analysis_data[i])
114+
total_str = human_readable_time(total_data[i])
115+
report.append(f"| {n_data[i]:<3} | {analysis_str:>13} | {total_str:>16} |")
116+
117+
report.append("")
118+
119+
# Complexity analysis
120+
report.append(f"**Complexity Analysis:**")
121+
try:
122+
popt, pcov = curve_fit(power_law, n_data, analysis_data, p0=[0, 2], maxfev=5000)
123+
_, k = popt
124+
125+
# R-squared calculation
126+
residuals = analysis_data - power_law(n_data, *popt)
127+
ss_res = np.sum(residuals**2)
128+
ss_tot = np.sum((analysis_data - np.mean(analysis_data))**2)
129+
r_squared = 1 - (ss_res / ss_tot)
130+
131+
# Confidence Interval for k
132+
alpha = 0.05 # 95% confidence
133+
dof = max(0, len(n_data) - len(popt)) # degrees of freedom
134+
t_val = t.ppf(1.0 - alpha / 2., dof)
135+
# Standard error of the parameters
136+
perr = np.sqrt(np.diag(pcov))
137+
k_stderr = perr[1]
138+
k_ci_lower = k - t_val * k_stderr
139+
k_ci_upper = k + t_val * k_stderr
140+
141+
report.append(f"- The performance of the analysis for this case scales approximately as **O(n<sup>{k:.2f}</sup>)**.")
142+
report.append(f"- **Goodness of Fit (R²):** `{r_squared:.4f}` (closer to 1.0 is better).")
143+
report.append(f"- **95% Confidence Interval for exponent 'k':** `[{k_ci_lower:.2f}, {k_ci_upper:.2f}]`.")
144+
145+
except RuntimeError:
146+
report.append("- Could not determine a best-fit curve for the data.")
147+
148+
report.append("\n---\n")
149+
150+
return "\n".join(report)
151+
152+
def run_single_test(clang_binary: str, test_type: str, n: int) -> tuple[float, float]:
153+
"""Generates, compiles, and benchmarks a single test case."""
154+
print(f"--- Running Test: {test_type.capitalize()} with N={n} ---")
155+
156+
generated_code = ""
157+
if test_type == 'cycle':
158+
generated_code = generate_cpp_cycle_test(n)
159+
else: # merge
160+
generated_code = generate_cpp_merge_test(n)
161+
162+
with tempfile.NamedTemporaryFile(mode='w+', suffix='.cpp', delete=False) as tmp_cpp:
163+
tmp_cpp.write(generated_code)
164+
source_file = tmp_cpp.name
165+
166+
trace_file = os.path.splitext(source_file)[0] + '.json'
167+
168+
clang_command = [
169+
clang_binary, '-c', '-o', '/dev/null', '-ftime-trace=' + trace_file,
170+
'-Wexperimental-lifetime-safety', '-std=c++17', source_file
171+
]
172+
173+
result = subprocess.run(clang_command, capture_output=True, text=True)
174+
175+
if result.returncode != 0:
176+
print(f"Compilation failed for N={n}!", file=sys.stderr)
177+
print(result.stderr, file=sys.stderr)
178+
os.remove(source_file)
179+
return 0.0, 0.0
180+
181+
lifetime_us, total_us = analyze_trace_file(trace_file)
182+
os.remove(source_file)
183+
os.remove(trace_file)
184+
185+
return lifetime_us / 1000.0, total_us / 1000.0
186+
187+
if __name__ == "__main__":
188+
parser = argparse.ArgumentParser(description="Generate, compile, and benchmark C++ test cases for Clang's lifetime analysis.")
189+
parser.add_argument("--clang-binary", type=str, required=True, help="Path to the Clang executable.")
190+
191+
args = parser.parse_args()
192+
193+
n_values = [10, 25, 50, 75, 100, 150, 200]
194+
results = {
195+
'cycle': {'n': [], 'lifetime_ms': [], 'total_ms': []},
196+
'merge': {'n': [], 'lifetime_ms': [], 'total_ms': []}
197+
}
198+
199+
print("Running performance benchmarks...")
200+
for test_type in ['cycle', 'merge']:
201+
for n in n_values:
202+
lifetime_ms, total_ms = run_single_test(args.clang_binary, test_type, n)
203+
if total_ms > 0:
204+
results[test_type]['n'].append(n)
205+
results[test_type]['lifetime_ms'].append(lifetime_ms)
206+
results[test_type]['total_ms'].append(total_ms)
207+
print(f" Total: {human_readable_time(total_ms)} | Analysis: {human_readable_time(lifetime_ms)}")
208+
209+
print("\n\n" + "="*80)
210+
print("Generating Markdown Report...")
211+
print("="*80 + "\n")
212+
213+
markdown_report = generate_markdown_report(results)
214+
print(markdown_report)
215+

0 commit comments

Comments
 (0)