Skip to content

Commit cb9a521

Browse files
committed
add-loan-analysis-to-benchmark
1 parent c29040a commit cb9a521

File tree

1 file changed

+157
-81
lines changed

1 file changed

+157
-81
lines changed

clang/test/Analysis/LifetimeSafety/benchmark.py

Lines changed: 157 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,12 @@ def generate_cpp_cycle_test(n: int) -> str:
2121
struct MyObj { int id; ~MyObj() {} };
2222
2323
void long_cycle_4(bool condition) {
24-
MyObj v1{1};
25-
MyObj v2{1};
26-
MyObj v3{1};
27-
MyObj v4{1};
28-
29-
MyObj* p1 = &v1;
30-
MyObj* p2 = &v2;
31-
MyObj* p3 = &v3;
32-
MyObj* p4 = &v4;
24+
MyObj v1{1}; MyObj v2{1}; MyObj v3{1}; MyObj v4{1};
25+
MyObj* p1 = &v1; MyObj* p2 = &v2; MyObj* p3 = &v3; MyObj* p4 = &v4;
3326
3427
while (condition) {
3528
MyObj* temp = p1;
36-
p1 = p2;
37-
p2 = p3;
38-
p3 = p4;
39-
p4 = temp;
29+
p1 = p2; p2 = p3; p3 = p4; p4 = temp;
4030
}
4131
}
4232
"""
@@ -99,28 +89,81 @@ def generate_cpp_merge_test(n: int) -> str:
9989
return cpp_code
10090

10191

102-
def analyze_trace_file(trace_path: str) -> tuple[float, float]:
92+
def generate_cpp_nested_loop_test(n: int) -> str:
10393
"""
104-
Parses the -ftime-trace JSON output to find durations.
94+
Generates C++ code with N levels of nested loops.
95+
This pattern tests how analysis performance scales with loop nesting depth,
96+
which is a key factor in the complexity of dataflow analyses on structured
97+
control flow.
98+
99+
Example (n=3):
100+
struct MyObj { int id; ~MyObj() {} };
101+
void nested_loops_3() {
102+
MyObj* p = nullptr;
103+
for(int i0=0; i0<2; ++i0) {
104+
MyObj s0; p = &s0;
105+
for(int i1=0; i1<2; ++i1) {
106+
MyObj s1; p = &s1;
107+
for(int i2=0; i2<2; ++i2) {
108+
MyObj s2; p = &s2;
109+
}
110+
}
111+
}
112+
}
113+
"""
114+
if n <= 0:
115+
return "// Nesting depth must be positive."
116+
117+
cpp_code = "struct MyObj { int id; ~MyObj() {} };\n\n"
118+
cpp_code += f"void nested_loops_{n}() {{\n"
119+
cpp_code += " MyObj* p = nullptr;\n"
120+
121+
for i in range(n):
122+
indent = " " * (i + 1)
123+
cpp_code += f"{indent}for(int i{i}=0; i{i}<2; ++i{i}) {{\n"
124+
cpp_code += f"{indent} MyObj s{i}; p = &s{i};\n"
125+
126+
for i in range(n - 1, -1, -1):
127+
indent = " " * (i + 1)
128+
cpp_code += f"{indent}}}\n"
129+
130+
cpp_code += "}\n"
131+
cpp_code += f"\nint main() {{ nested_loops_{n}(); return 0; }}\n"
132+
return cpp_code
133+
105134

106-
Returns:
107-
A tuple of (lifetime_analysis_duration_us, total_clang_duration_us).
135+
def analyze_trace_file(trace_path: str) -> dict:
108136
"""
109-
lifetime_duration = 0.0
110-
total_duration = 0.0
137+
Parses the -ftime-trace JSON output to find durations for the lifetime
138+
analysis and its sub-phases.
139+
Returns a dictionary of durations in microseconds.
140+
"""
141+
durations = {
142+
"lifetime_us": 0.0,
143+
"total_us": 0.0,
144+
"fact_gen_us": 0.0,
145+
"loan_prop_us": 0.0,
146+
"expired_loans_us": 0.0,
147+
}
148+
event_name_map = {
149+
"LifetimeSafetyAnalysis": "lifetime_us",
150+
"ExecuteCompiler": "total_us",
151+
"FactGenerator": "fact_gen_us",
152+
"LoanPropagation": "loan_prop_us",
153+
"ExpiredLoans": "expired_loans_us",
154+
}
111155
try:
112156
with open(trace_path, "r") as f:
113157
trace_data = json.load(f)
114158
for event in trace_data.get("traceEvents", []):
115-
if event.get("name") == "LifetimeSafetyAnalysis":
116-
lifetime_duration += float(event.get("dur", 0))
117-
if event.get("name") == "ExecuteCompiler":
118-
total_duration += float(event.get("dur", 0))
119-
159+
event_name = event.get("name")
160+
if event_name in event_name_map:
161+
key = event_name_map[event_name]
162+
durations[key] += float(event.get("dur", 0))
120163
except (IOError, json.JSONDecodeError) as e:
121164
print(f"Error reading or parsing trace file {trace_path}: {e}", file=sys.stderr)
122-
return 0.0, 0.0
123-
return lifetime_duration, total_duration
165+
return {key: 0.0 for key in durations}
166+
return durations
124167

125168

126169
def power_law(n, c, k):
@@ -135,8 +178,29 @@ def human_readable_time(ms: float) -> str:
135178
return f"{ms:.2f} ms"
136179

137180

181+
def calculate_complexity(n_data, y_data) -> tuple[float | None, float | None]:
182+
"""
183+
Calculates the exponent 'k' for the power law fit y = c * n^k.
184+
Returns a tuple of (k, k_standard_error).
185+
"""
186+
try:
187+
if len(n_data) < 3 or np.all(y_data < 1e-6) or np.var(y_data) < 1e-6:
188+
return None, None
189+
190+
non_zero_indices = y_data > 0
191+
if np.sum(non_zero_indices) < 3:
192+
return None, None
193+
194+
n_fit, y_fit = n_data[non_zero_indices], y_data[non_zero_indices]
195+
popt, pcov = curve_fit(power_law, n_fit, y_fit, p0=[0, 1], maxfev=5000)
196+
k_stderr = np.sqrt(np.diag(pcov))[1]
197+
return popt[1], k_stderr
198+
except (RuntimeError, ValueError):
199+
return None, None
200+
201+
138202
def generate_markdown_report(results: dict) -> str:
139-
"""Generates a Markdown-formatted report from the benchmark results."""
203+
"""Generates a concise, Markdown-formatted report from the benchmark results."""
140204
report = []
141205
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")
142206
report.append(f"# Lifetime Analysis Performance Report")
@@ -146,54 +210,51 @@ def generate_markdown_report(results: dict) -> str:
146210
for test_name, data in results.items():
147211
title = data["title"]
148212
report.append(f"## Test Case: {title}")
149-
report.append("")
213+
report.append("\n**Relative Timing Results (% of Total Clang Time):**\n")
150214

151215
# Table header
152-
report.append("| N | Analysis Time | Total Clang Time |")
153-
report.append("|:----|--------------:|-----------------:|")
216+
report.append(
217+
"| N (Input Size) | Total Analysis | Fact Generator | Loan Propagation | Expired Loans |"
218+
)
219+
report.append(
220+
"|:---------------|---------------:|---------------:|-----------------:|--------------:|"
221+
)
154222

155223
# Table rows
156224
n_data = np.array(data["n"])
157-
analysis_data = np.array(data["lifetime_ms"])
158-
total_data = np.array(data["total_ms"])
225+
total_ms_data = np.array(data["total_ms"])
159226
for i in range(len(n_data)):
160-
analysis_str = human_readable_time(analysis_data[i])
161-
total_str = human_readable_time(total_data[i])
162-
report.append(f"| {n_data[i]:<3} | {analysis_str:>13} | {total_str:>16} |")
163-
164-
report.append("")
165-
166-
# Complexity analysis
167-
report.append(f"**Complexity Analysis:**")
168-
try:
169-
# Curve fitting requires at least 3 points
170-
if len(n_data) < 3:
171-
raise ValueError("Not enough data points to perform curve fitting.")
172-
173-
popt, pcov = curve_fit(
174-
power_law, n_data, analysis_data, p0=[0, 2], maxfev=5000
175-
)
176-
_, k = popt
177-
178-
# Confidence Interval for k
179-
alpha = 0.05 # 95% confidence
180-
dof = max(0, len(n_data) - len(popt)) # degrees of freedom
181-
t_val = t.ppf(1.0 - alpha / 2.0, dof)
182-
# Standard error of the parameters
183-
perr = np.sqrt(np.diag(pcov))
184-
k_stderr = perr[1]
185-
k_ci_lower = k - t_val * k_stderr
186-
k_ci_upper = k + t_val * k_stderr
187-
188-
report.append(
189-
f"- The performance for this case scales approx. as **O(n<sup>{k:.2f}</sup>)**."
190-
)
191-
report.append(
192-
f"- **95% Confidence interval for exponent:** `[{k_ci_lower:.2f}, {k_ci_upper:.2f}]`."
193-
)
194-
195-
except (RuntimeError, ValueError) as e:
196-
report.append(f"- Could not determine a best-fit curve for the data: {e}")
227+
total_t = total_ms_data[i]
228+
if total_t < 1e-6:
229+
total_t = 1.0 # Avoid division by zero
230+
231+
row = [
232+
f"| {n_data[i]:<14}",
233+
f"{data['lifetime_ms'][i] / total_t * 100:>13.2f}% |",
234+
f"{data['fact_gen_ms'][i] / total_t * 100:>14.2f}% |",
235+
f"{data['loan_prop_ms'][i] / total_t * 100:>17.2f}% |",
236+
f"{data['expired_loans_ms'][i] / total_t * 100:>13.2f}% |",
237+
]
238+
report.append(" ".join(row))
239+
240+
report.append("\n**Complexity Analysis:**\n")
241+
report.append("| Analysis Phase | Complexity O(n<sup>k</sup>) |")
242+
report.append("|:------------------|:--------------------------|")
243+
244+
analysis_phases = {
245+
"Total Analysis": data["lifetime_ms"],
246+
"FactGenerator": data["fact_gen_ms"],
247+
"LoanPropagation": data["loan_prop_ms"],
248+
"ExpiredLoans": data["expired_loans_ms"],
249+
}
250+
251+
for phase_name, y_data in analysis_phases.items():
252+
k, delta = calculate_complexity(n_data, np.array(y_data))
253+
if k is not None and delta is not None:
254+
complexity_str = f"O(n<sup>{k:.2f}</sup> &pm; {delta:.2f})"
255+
else:
256+
complexity_str = "~ O(1) (Negligible)"
257+
report.append(f"| {phase_name:<17} | {complexity_str:<25} |")
197258

198259
report.append("\n---\n")
199260

@@ -202,12 +263,11 @@ def generate_markdown_report(results: dict) -> str:
202263

203264
def run_single_test(
204265
clang_binary: str, output_dir: str, test_name: str, generator_func, n: int
205-
) -> tuple[float, float]:
266+
) -> dict:
206267
"""Generates, compiles, and benchmarks a single test case."""
207268
print(f"--- Running Test: {test_name.capitalize()} with N={n} ---")
208269

209270
generated_code = generator_func(n)
210-
211271
base_name = f"test_{test_name}_{n}"
212272
source_file = os.path.join(output_dir, f"{base_name}.cpp")
213273
trace_file = os.path.join(output_dir, f"{base_name}.json")
@@ -225,17 +285,15 @@ def run_single_test(
225285
"-std=c++17",
226286
source_file,
227287
]
228-
229288
result = subprocess.run(clang_command, capture_output=True, text=True)
230289

231290
if result.returncode != 0:
232291
print(f"Compilation failed for N={n}!", file=sys.stderr)
233292
print(result.stderr, file=sys.stderr)
234-
return 0.0, 0.0
293+
return {}
235294

236-
lifetime_us, total_us = analyze_trace_file(trace_file)
237-
238-
return lifetime_us / 1000.0, total_us / 1000.0
295+
durations_us = analyze_trace_file(trace_file)
296+
return {key.replace('_us', '_ms'): value / 1000.0 for key, value in durations_us.items()}
239297

240298

241299
if __name__ == "__main__":
@@ -270,6 +328,12 @@ def run_single_test(
270328
"generator_func": generate_cpp_merge_test,
271329
"n_values": [10, 50, 100, 200, 400, 800],
272330
},
331+
{
332+
"name": "nested_loops",
333+
"title": "Deeply Nested Loops",
334+
"generator_func": generate_cpp_nested_loop_test,
335+
"n_values": [10, 50, 100, 200, 400, 800],
336+
},
273337
]
274338

275339
results = {}
@@ -282,21 +346,28 @@ def run_single_test(
282346
"n": [],
283347
"lifetime_ms": [],
284348
"total_ms": [],
349+
"fact_gen_ms": [],
350+
"loan_prop_ms": [],
351+
"expired_loans_ms": [],
285352
}
286353
for n in config["n_values"]:
287-
lifetime_ms, total_ms = run_single_test(
354+
durations_ms = run_single_test(
288355
args.clang_binary,
289356
args.output_dir,
290357
test_name,
291358
config["generator_func"],
292359
n,
293360
)
294-
if total_ms > 0:
361+
if durations_ms:
295362
results[test_name]["n"].append(n)
296-
results[test_name]["lifetime_ms"].append(lifetime_ms)
297-
results[test_name]["total_ms"].append(total_ms)
363+
for key, value in durations_ms.items():
364+
results[test_name][key].append(value)
365+
298366
print(
299-
f" Total: {human_readable_time(total_ms)} | Analysis: {human_readable_time(lifetime_ms)}"
367+
f" Total Analysis: {human_readable_time(durations_ms['lifetime_ms'])} | "
368+
f"FactGen: {human_readable_time(durations_ms['fact_gen_ms'])} | "
369+
f"LoanProp: {human_readable_time(durations_ms['loan_prop_ms'])} | "
370+
f"ExpiredLoans: {human_readable_time(durations_ms['expired_loans_ms'])}"
300371
)
301372

302373
print("\n\n" + "=" * 80)
@@ -305,3 +376,8 @@ def run_single_test(
305376

306377
markdown_report = generate_markdown_report(results)
307378
print(markdown_report)
379+
380+
report_filename = os.path.join(args.output_dir, "performance_report.md")
381+
with open(report_filename, "w") as f:
382+
f.write(markdown_report)
383+
print(f"Report saved to: {report_filename}")

0 commit comments

Comments
 (0)