kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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()