cc_bench_gen.py (12091B)
1 #!/usr/bin/env python3 2 """Synthetic C input generator for the -O0 compile+link scaling benchmark. 3 4 This is the single source of truth for the benchmark's *axes*: each axis varies 5 one input dimension (function count, statement count, object-file count, ...) so 6 the harness can sweep it over a geometric size series and the reporter can fit a 7 scaling exponent. The goal is linear scaling on every axis; a superlinear axis 8 points straight at an O(n^2) trap in the single-pass frontend or the linker. 9 10 Two modes: 11 12 cc_bench_gen.py --list 13 Print a JSON array describing every axis: name, phase (compile|link), 14 unit label, timed mode (compile=-c, preprocess=-E, link), and the default 15 geometric size series. The harness drives the sweep from this. 16 17 cc_bench_gen.py --axis AXIS --n N --out DIR 18 Write the source/header files for AXIS at size N into DIR and print a JSON 19 manifest describing how to build/time/check this instance. The manifest 20 names *intent* (mode, source, prebuild list, link order, expected exit), 21 never tool-specific flags -- the harness turns that into kit/clang command 22 lines so the axis definitions stay tool-neutral. 23 24 Generated code is dead-simple C11 with no platform headers (integer arithmetic 25 + a deterministic main return % 256), so compile time is frontend/codegen-bound 26 and identical for kit and the clang reference. 27 """ 28 import argparse 29 import json 30 import os 31 import sys 32 33 # --------------------------------------------------------------------------- 34 # Axis catalog. Each entry: phase, unit label, timed mode, default size series. 35 # Sizes are tuned per axis: large where the work is cheap per unit (statements, 36 # macro expansions), smaller where an O(n^2) trap would blow up fast (locals, 37 # decls) or where each unit is its own file (objects, headers). 38 # --------------------------------------------------------------------------- 39 AXES = { 40 # ---- compile axes (kit cc -O0 -c, or -E for the preprocessor axes) ---- 41 "fn-count": { 42 "phase": "compile", "unit": "functions", "mode": "compile", 43 "sizes": [1000, 2000, 4000, 8000, 16000, 32000], 44 "blurb": "N tiny functions -> symbol-table inserts, per-fn codegen+emit", 45 }, 46 "body-size": { 47 "phase": "compile", "unit": "statements", "mode": "compile", 48 "sizes": [4000, 8000, 16000, 32000, 64000, 128000], 49 "blurb": "one function, N statements -> per-stmt codegen, value stack, " 50 "BB patch-ups, const-tracker", 51 }, 52 "global-decl": { 53 "phase": "compile", "unit": "globals", "mode": "compile", 54 "sizes": [2000, 4000, 8000, 16000, 32000, 64000], 55 "blurb": "N file-scope globals -> global scope table inserts", 56 }, 57 "type-decl": { 58 "phase": "compile", "unit": "types", "mode": "compile", 59 "sizes": [2000, 4000, 8000, 16000, 32000, 64000], 60 "blurb": "N distinct struct typedefs -> type/typedef interning", 61 }, 62 "locals-per-fn": { 63 "phase": "compile", "unit": "locals", "mode": "compile", 64 "sizes": [1000, 2000, 4000, 8000, 16000, 32000], 65 "blurb": "one function, N locals -> local scope table, frame slots", 66 }, 67 "pp-macro": { 68 "phase": "compile", "unit": "expansions", "mode": "preprocess", 69 "sizes": [4000, 8000, 16000, 32000, 64000, 128000], 70 "blurb": "N function-like macro expansions -> macro expander, token buf", 71 }, 72 "pp-include": { 73 "phase": "compile", "unit": "headers", "mode": "preprocess", 74 "sizes": [500, 1000, 2000, 4000, 8000], 75 "blurb": "N distinct headers included once -> lexer/pp file handling", 76 }, 77 "ref-density": { 78 "phase": "compile", "unit": "calls", "mode": "compile", 79 "sizes": [2000, 4000, 8000, 16000, 32000, 64000], 80 "blurb": "one function, N calls to N distinct externs -> call lowering, " 81 "relocation emission", 82 }, 83 # ---- link axes (objects pre-built untimed; only kit ld is timed) ---- 84 "obj-count": { 85 "phase": "link", "unit": "objects", "mode": "link", 86 "sizes": [32, 64, 128, 256, 512, 1024], 87 "blurb": "N objects linked to one exe -> input handling, symbol resolve " 88 "map, layout/section merge", 89 }, 90 "symbol-count": { 91 "phase": "link", "unit": "symbols", "mode": "link", 92 "sizes": [2000, 4000, 8000, 16000, 32000, 64000], 93 "blurb": "fixed object count, N total symbols/relocs -> global symbol " 94 "hash map + reloc apply", 95 }, 96 } 97 98 99 def _write(path, lines): 100 """Write a file from an iterable of lines (joined, single write).""" 101 with open(path, "w") as f: 102 f.write("\n".join(lines)) 103 f.write("\n") 104 105 106 # --------------------------------------------------------------------------- 107 # Per-axis source generators. Each returns the manifest dict (minus axis/n/unit/ 108 # phase, which the caller fills in) after writing files into `out`. 109 # --------------------------------------------------------------------------- 110 def gen_fn_count(n, out): 111 lines = ["/* fn-count n=%d */" % n] 112 for i in range(n): 113 lines.append("int f_%d(int x){return x + %d;}" % (i, i % 251)) 114 _write(os.path.join(out, "gen.c"), lines) 115 return {"mode": "compile", "source": "gen.c"} 116 117 118 def gen_body_size(n, out): 119 lines = ["/* body-size n=%d */" % n, "int body(int x){", " int acc = x;"] 120 for i in range(n): 121 lines.append(" acc = acc * 1000003 + %d;" % (i % 1000)) 122 lines += [" return acc;", "}"] 123 _write(os.path.join(out, "gen.c"), lines) 124 return {"mode": "compile", "source": "gen.c"} 125 126 127 def gen_global_decl(n, out): 128 lines = ["/* global-decl n=%d */" % n] 129 for i in range(n): 130 lines.append("int g_%d = %d;" % (i, i % 257)) 131 # Touch the first and last so the TU is unambiguously meaningful. 132 lines.append("int gd_use(void){return g_0 + g_%d;}" % (n - 1)) 133 _write(os.path.join(out, "gen.c"), lines) 134 return {"mode": "compile", "source": "gen.c"} 135 136 137 def gen_type_decl(n, out): 138 lines = ["/* type-decl n=%d */" % n] 139 for i in range(n): 140 lines.append("typedef struct { int a; int b; } T_%d;" % i) 141 # Use the last type so the table is actually consulted, not just filled. 142 lines.append("T_%d g_last;" % (n - 1)) 143 lines.append("int td_use(void){return g_last.a + g_last.b;}") 144 _write(os.path.join(out, "gen.c"), lines) 145 return {"mode": "compile", "source": "gen.c"} 146 147 148 def gen_locals_per_fn(n, out): 149 lines = ["/* locals-per-fn n=%d */" % n, "int locals(int x){"] 150 lines.append(" int v0 = x;") 151 for i in range(1, n): 152 lines.append(" int v%d = v%d + %d;" % (i, i - 1, i % 251)) 153 lines.append(" return v0 ^ v%d;" % (n - 1)) 154 lines.append("}") 155 _write(os.path.join(out, "gen.c"), lines) 156 return {"mode": "compile", "source": "gen.c"} 157 158 159 def gen_pp_macro(n, out): 160 lines = ["/* pp-macro n=%d */" % n, 161 "#define SQ(x) ((x)*(x))", 162 "#define MIX(a,b) (SQ(a) + SQ(b) - (a)*(b))", 163 "int pp_macro(int x){", " int a = 0;"] 164 for i in range(n): 165 lines.append(" a += MIX(x + %d, x - %d);" % (i % 997, i % 991)) 166 lines += [" return a;", "}"] 167 _write(os.path.join(out, "gen.c"), lines) 168 return {"mode": "preprocess", "source": "gen.c"} 169 170 171 def gen_pp_include(n, out): 172 for i in range(n): 173 guard = "H_%d_H" % i 174 _write(os.path.join(out, "h_%d.h" % i), [ 175 "#ifndef %s" % guard, "#define %s" % guard, 176 "typedef int hi_%d_t;" % i, 177 "extern hi_%d_t hv_%d;" % (i, i), 178 "#endif", 179 ]) 180 lines = ["/* pp-include n=%d */" % n] 181 for i in range(n): 182 lines.append('#include "h_%d.h"' % i) 183 lines.append("int pp_include(void){return 0;}") 184 _write(os.path.join(out, "gen.c"), lines) 185 return {"mode": "preprocess", "source": "gen.c", "incdir": "."} 186 187 188 def gen_ref_density(n, out): 189 lines = ["/* ref-density n=%d */" % n] 190 for i in range(n): 191 lines.append("extern int rf_%d(void);" % i) 192 lines += ["int hub(void){", " int s = 0;"] 193 for i in range(n): 194 lines.append(" s += rf_%d();" % i) 195 lines += [" return s;", "}"] 196 _write(os.path.join(out, "gen.c"), lines) 197 return {"mode": "compile", "source": "gen.c"} 198 199 200 def gen_obj_count(n, out): 201 # N leaf objects, each defining leaf_i returning i. A main sums them all and 202 # returns the total % 256. Objects are pre-built untimed; only the link of 203 # all N+1 objects is timed. 204 prebuild = [] 205 link_order = [] 206 for i in range(n): 207 _write(os.path.join(out, "obj_%d.c" % i), 208 ["int leaf_%d(void){return %d;}" % (i, i % 256)]) 209 prebuild.append("obj_%d.c" % i) 210 link_order.append("obj_%d.o" % i) 211 main = ["/* obj-count main n=%d */" % n] 212 for i in range(n): 213 main.append("extern int leaf_%d(void);" % i) 214 main += ["int main(void){", " long s = 0;"] 215 for i in range(n): 216 main.append(" s += leaf_%d();" % i) 217 expected = (sum(i % 256 for i in range(n))) % 256 218 main += [" return (int)(s % 256);", "}"] 219 _write(os.path.join(out, "main.c"), main) 220 prebuild.append("main.c") 221 link_order.append("main.o") 222 return {"mode": "link", "prebuild": prebuild, "link_order": link_order, 223 "expected_exit": expected} 224 225 226 def gen_symbol_count(n, out): 227 # Fixed object count (2): defs.c defines N functions, main.c references all 228 # N. Link must resolve N symbols and apply N relocations -> stresses the 229 # global symbol hash map + reloc apply with object count held constant. 230 defs = ["/* symbol-count defs n=%d */" % n] 231 for i in range(n): 232 defs.append("int s_%d(void){return %d;}" % (i, i % 256)) 233 _write(os.path.join(out, "defs.c"), defs) 234 main = ["/* symbol-count main n=%d */" % n] 235 for i in range(n): 236 main.append("extern int s_%d(void);" % i) 237 main += ["int main(void){", " long s = 0;"] 238 for i in range(n): 239 main.append(" s += s_%d();" % i) 240 main += [" return (int)(s % 256);", "}"] 241 _write(os.path.join(out, "main.c"), main) 242 expected = (sum(i % 256 for i in range(n))) % 256 243 return {"mode": "link", "prebuild": ["defs.c", "main.c"], 244 "link_order": ["defs.o", "main.o"], "expected_exit": expected} 245 246 247 GENERATORS = { 248 "fn-count": gen_fn_count, 249 "body-size": gen_body_size, 250 "global-decl": gen_global_decl, 251 "type-decl": gen_type_decl, 252 "locals-per-fn": gen_locals_per_fn, 253 "pp-macro": gen_pp_macro, 254 "pp-include": gen_pp_include, 255 "ref-density": gen_ref_density, 256 "obj-count": gen_obj_count, 257 "symbol-count": gen_symbol_count, 258 } 259 260 261 def cmd_list(): 262 out = [] 263 for name, spec in AXES.items(): 264 out.append({ 265 "axis": name, 266 "phase": spec["phase"], 267 "unit": spec["unit"], 268 "mode": spec["mode"], 269 "sizes": spec["sizes"], 270 "blurb": spec["blurb"], 271 }) 272 json.dump(out, sys.stdout, indent=2) 273 sys.stdout.write("\n") 274 275 276 def cmd_gen(axis, n, out): 277 if axis not in GENERATORS: 278 sys.exit("cc_bench_gen: unknown axis %r" % axis) 279 if n < 1: 280 sys.exit("cc_bench_gen: n must be >= 1") 281 os.makedirs(out, exist_ok=True) 282 spec = AXES[axis] 283 manifest = GENERATORS[axis](n, out) 284 manifest.update({ 285 "axis": axis, "n": n, "phase": spec["phase"], "unit": spec["unit"], 286 }) 287 json.dump(manifest, sys.stdout) 288 sys.stdout.write("\n") 289 290 291 def main(): 292 ap = argparse.ArgumentParser(description=__doc__) 293 ap.add_argument("--list", action="store_true", 294 help="print the axis catalog as JSON and exit") 295 ap.add_argument("--axis", help="axis name (see --list)") 296 ap.add_argument("--n", type=int, help="size for this axis") 297 ap.add_argument("--out", help="output directory for generated files") 298 args = ap.parse_args() 299 300 if args.list: 301 cmd_list() 302 return 303 if not (args.axis and args.n is not None and args.out): 304 ap.error("need --axis, --n and --out (or --list)") 305 cmd_gen(args.axis, args.n, args.out) 306 307 308 if __name__ == "__main__": 309 main()