9
9
from scipy .optimize import curve_fit
10
10
from scipy .stats import t
11
11
12
+
12
13
def generate_cpp_cycle_test (n : int ) -> str :
13
14
"""
14
15
Generates a C++ code snippet with a specified number of pointers in a cycle.
16
+ Example:
17
+ struct MyObj { int id; ~MyObj() {} };
18
+
19
+ void long_cycle_4(bool condition) {
20
+ MyObj v1{1};
21
+ MyObj v2{1};
22
+ MyObj v3{1};
23
+ MyObj v4{1};
24
+
25
+ MyObj* p1 = &v1;
26
+ MyObj* p2 = &v2;
27
+ MyObj* p3 = &v3;
28
+ MyObj* p4 = &v4;
29
+
30
+ while (condition) {
31
+ MyObj* temp = p1;
32
+ p1 = p2;
33
+ p2 = p3;
34
+ p3 = p4;
35
+ p4 = temp;
36
+ }
37
+ }
15
38
"""
16
39
if n <= 0 :
17
40
return "// Number of variables must be positive."
@@ -34,9 +57,22 @@ def generate_cpp_cycle_test(n: int) -> str:
34
57
cpp_code += f"\n int main() {{ long_cycle_{ n } (false); return 0; }}\n "
35
58
return cpp_code
36
59
60
+
37
61
def generate_cpp_merge_test (n : int ) -> str :
38
62
"""
39
63
Generates a C++ code snippet with N independent conditional assignments.
64
+ Example:
65
+ struct MyObj { int id; ~MyObj() {} };
66
+
67
+ void conditional_merges_4(bool condition) {
68
+ MyObj v1, v2, v3, v4;
69
+ MyObj *p1 = nullptr, *p2 = nullptr, *p3 = nullptr, *p4 = nullptr;
70
+
71
+ if(condition) { p1 = &v1; }
72
+ if(condition) { p2 = &v2; }
73
+ if(condition) { p3 = &v3; }
74
+ if(condition) { p4 = &v4; }
75
+ }
40
76
"""
41
77
if n <= 0 :
42
78
return "// Number of variables must be positive."
@@ -55,6 +91,7 @@ def generate_cpp_merge_test(n: int) -> str:
55
91
cpp_code += f"\n int main() {{ conditional_merges_{ n } (false); return 0; }}\n "
56
92
return cpp_code
57
93
94
+
58
95
def analyze_trace_file (trace_path : str ) -> tuple [float , float ]:
59
96
"""
60
97
Parses the -ftime-trace JSON output to find durations.
@@ -65,29 +102,32 @@ def analyze_trace_file(trace_path: str) -> tuple[float, float]:
65
102
lifetime_duration = 0.0
66
103
total_duration = 0.0
67
104
try :
68
- with open (trace_path , 'r' ) as f :
105
+ with open (trace_path , "r" ) as f :
69
106
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 ))
107
+ for event in trace_data .get (" traceEvents" , []):
108
+ if event .get (" name" ) == "LifetimeSafetyAnalysis" :
109
+ lifetime_duration += float (event .get (" dur" , 0 ))
110
+ if event .get (" name" ) == " ExecuteCompiler" :
111
+ total_duration += float (event .get (" dur" , 0 ))
75
112
76
113
except (IOError , json .JSONDecodeError ) as e :
77
114
print (f"Error reading or parsing trace file { trace_path } : { e } " , file = sys .stderr )
78
115
return 0.0 , 0.0
79
116
return lifetime_duration , total_duration
80
117
118
+
81
119
def power_law (n , c , k ):
82
120
"""Represents the power law function: y = c * n^k"""
83
121
return c * np .power (n , k )
84
122
123
+
85
124
def human_readable_time (ms : float ) -> str :
86
125
"""Converts milliseconds to a human-readable string (ms or s)."""
87
126
if ms >= 1000 :
88
127
return f"{ ms / 1000 :.2f} s"
89
128
return f"{ ms :.2f} ms"
90
129
130
+
91
131
def generate_markdown_report (results : dict ) -> str :
92
132
"""Generates a Markdown-formatted report from the benchmark results."""
93
133
report = []
@@ -97,7 +137,7 @@ def generate_markdown_report(results: dict) -> str:
97
137
report .append ("\n ---\n " )
98
138
99
139
for test_type , data in results .items ():
100
- title = ' Pointer Cycle in Loop' if test_type == ' cycle' else ' CFG Merges'
140
+ title = " Pointer Cycle in Loop" if test_type == " cycle" else " CFG Merges"
101
141
report .append (f"## Test Case: { title } " )
102
142
report .append ("" )
103
143
@@ -106,9 +146,9 @@ def generate_markdown_report(results: dict) -> str:
106
146
report .append ("|:----|--------------:|-----------------:|" )
107
147
108
148
# 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' ])
149
+ n_data = np .array (data ["n" ])
150
+ analysis_data = np .array (data [" lifetime_ms" ])
151
+ total_data = np .array (data [" total_ms" ])
112
152
for i in range (len (n_data )):
113
153
analysis_str = human_readable_time (analysis_data [i ])
114
154
total_str = human_readable_time (total_data [i ])
@@ -119,28 +159,36 @@ def generate_markdown_report(results: dict) -> str:
119
159
# Complexity analysis
120
160
report .append (f"**Complexity Analysis:**" )
121
161
try :
122
- popt , pcov = curve_fit (power_law , n_data , analysis_data , p0 = [0 , 2 ], maxfev = 5000 )
162
+ popt , pcov = curve_fit (
163
+ power_law , n_data , analysis_data , p0 = [0 , 2 ], maxfev = 5000
164
+ )
123
165
_ , k = popt
124
-
166
+
125
167
# R-squared calculation
126
168
residuals = analysis_data - power_law (n_data , * popt )
127
169
ss_res = np .sum (residuals ** 2 )
128
- ss_tot = np .sum ((analysis_data - np .mean (analysis_data ))** 2 )
170
+ ss_tot = np .sum ((analysis_data - np .mean (analysis_data )) ** 2 )
129
171
r_squared = 1 - (ss_res / ss_tot )
130
-
172
+
131
173
# Confidence Interval for k
132
174
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 )
175
+ dof = max (0 , len (n_data ) - len (popt )) # degrees of freedom
176
+ t_val = t .ppf (1.0 - alpha / 2.0 , dof )
135
177
# Standard error of the parameters
136
178
perr = np .sqrt (np .diag (pcov ))
137
179
k_stderr = perr [1 ]
138
180
k_ci_lower = k - t_val * k_stderr
139
181
k_ci_upper = k + t_val * k_stderr
140
182
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} ]`." )
183
+ report .append (
184
+ f"- The performance of the analysis for this case scales approximately as **O(n<sup>{ k :.2f} </sup>)**."
185
+ )
186
+ report .append (
187
+ f"- **Goodness of Fit (R<sup>2</sup>):** `{ r_squared :.4f} ` (closer to 1.0 is better)."
188
+ )
189
+ report .append (
190
+ f"- **95% Confidence Interval for exponent 'k':** `[{ k_ci_lower :.2f} , { k_ci_upper :.2f} ]`."
191
+ )
144
192
145
193
except RuntimeError :
146
194
report .append ("- Could not determine a best-fit curve for the data." )
@@ -149,67 +197,82 @@ def generate_markdown_report(results: dict) -> str:
149
197
150
198
return "\n " .join (report )
151
199
200
+
152
201
def run_single_test (clang_binary : str , test_type : str , n : int ) -> tuple [float , float ]:
153
202
"""Generates, compiles, and benchmarks a single test case."""
154
203
print (f"--- Running Test: { test_type .capitalize ()} with N={ n } ---" )
155
-
204
+
156
205
generated_code = ""
157
- if test_type == ' cycle' :
206
+ if test_type == " cycle" :
158
207
generated_code = generate_cpp_cycle_test (n )
159
- else : # merge
208
+ else : # merge
160
209
generated_code = generate_cpp_merge_test (n )
161
210
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
211
+ # Use a temporary directory to manage the source and trace files.
212
+ # The directory and its contents will be cleaned up automatically on exit.
213
+ with tempfile .TemporaryDirectory () as tmpdir :
214
+ base_name = f"test_{ test_type } _{ n } "
215
+ source_file = os .path .join (tmpdir , f"{ base_name } .cpp" )
216
+ trace_file = os .path .join (tmpdir , f"{ base_name } .json" )
217
+
218
+ with open (source_file , "w" ) as f :
219
+ f .write (generated_code )
220
+
221
+ clang_command = [
222
+ clang_binary ,
223
+ "-c" ,
224
+ "-o" ,
225
+ "/dev/null" ,
226
+ "-ftime-trace=" + trace_file ,
227
+ "-Wexperimental-lifetime-safety" ,
228
+ "-std=c++17" ,
229
+ source_file ,
230
+ ]
231
+
232
+ result = subprocess .run (clang_command , capture_output = True , text = True )
233
+
234
+ if result .returncode != 0 :
235
+ print (f"Compilation failed for N={ n } !" , file = sys .stderr )
236
+ print (result .stderr , file = sys .stderr )
237
+ # No need for manual cleanup, the 'with' statement handles it.
238
+ return 0.0 , 0.0
239
+
240
+ lifetime_us , total_us = analyze_trace_file (trace_file )
241
+
242
+ return lifetime_us / 1000.0 , total_us / 1000.0
243
+
186
244
187
245
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
-
246
+ parser = argparse .ArgumentParser (
247
+ description = "Generate, compile, and benchmark C++ test cases for Clang's lifetime analysis."
248
+ )
249
+ parser .add_argument (
250
+ "--clang-binary" , type = str , required = True , help = "Path to the Clang executable."
251
+ )
252
+
191
253
args = parser .parse_args ()
192
254
193
255
n_values = [10 , 25 , 50 , 75 , 100 , 150 , 200 ]
194
256
results = {
195
- ' cycle' : {'n' : [], ' lifetime_ms' : [], ' total_ms' : []},
196
- ' merge' : {'n' : [], ' lifetime_ms' : [], ' total_ms' : []}
257
+ " cycle" : {"n" : [], " lifetime_ms" : [], " total_ms" : []},
258
+ " merge" : {"n" : [], " lifetime_ms" : [], " total_ms" : []},
197
259
}
198
260
199
261
print ("Running performance benchmarks..." )
200
- for test_type in [' cycle' , ' merge' ]:
262
+ for test_type in [" cycle" , " merge" ]:
201
263
for n in n_values :
202
264
lifetime_ms , total_ms = run_single_test (args .clang_binary , test_type , n )
203
265
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 )
266
+ results [test_type ]["n" ].append (n )
267
+ results [test_type ]["lifetime_ms" ].append (lifetime_ms )
268
+ results [test_type ]["total_ms" ].append (total_ms )
269
+ print (
270
+ f" Total: { human_readable_time (total_ms )} | Analysis: { human_readable_time (lifetime_ms )} "
271
+ )
272
+
273
+ print ("\n \n " + "=" * 80 )
210
274
print ("Generating Markdown Report..." )
211
- print ("=" * 80 + "\n " )
212
-
275
+ print ("=" * 80 + "\n " )
276
+
213
277
markdown_report = generate_markdown_report (results )
214
278
print (markdown_report )
215
-
0 commit comments