kit

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

commit 8797d6e49ef2c4981803664a667297c39953fcae
parent 3f86328e70e2af348da45ab24097ca30e09db9d3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 11 Jun 2026 09:50:30 -0700

docs(perf): record Round 6 (constant-factor sprint +10-13%) + new structural frontier

Round 6 landed the four previously-listed levers (type-query gauntlet,
regcache scan, per-fn memset, token-pump sret) byte-identical, plus the
HW-SHA image-id link win. Updates the kit/clang ratio table and replaces
the forward-looking section with the measured structural frontier:
copy-and-patch codegen (~24x/op, Amdahl-capped ~1.66x; needs a full
stencil backend), 16B Tok (implemented byte-identical but neutral until
loc_intern is inlined), keyword-id-on-Sym (swap-in for the parser KwMap).
Records fused lex->pp->parse as a measured dead end (~5% of frontend).

Diffstat:
Mdoc/plan/PERF.md | 105++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
1 file changed, 69 insertions(+), 36 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -82,11 +82,29 @@ kit/clang at the largest measured size (lower = faster): | axis | kit/clang | axis | kit/clang | |---|--:|---|--:| -| locals-per-fn | 0.01× | type-decl | 0.55× | -| ref-density | 0.07× | pp-macro `-E` | 0.37× | -| fn-count | 0.08× | global-decl | 0.34× | -| obj-count (link) | 0.23× | pp-include `-E` | 0.80× | -| body-size | 0.27× | symbol-count (link) | 0.87× | +| locals-per-fn | 0.01× | type-decl | 0.45× | +| ref-density | 0.06× | pp-macro `-E` | 0.32× | +| fn-count | 0.08× | global-decl | 0.31× | +| obj-count (link) | 0.18× | pp-include `-E` | 0.86× | +| body-size | 0.25× | symbol-count (link) | 0.80× | + +**Round 6 (constant-factor sprint, gate: `scripts/perf_identity_gate.sh`).** A +byte-identical pass across all nine segments cut **+10–13%** off the major +compile axes (paired best-of-15 vs the pre-sprint binary: type-decl +13.3%, +pp-macro `-E` +10.8%, fn-count +10.1%, body-size +9.6%, ref-density +7%, +global-decl +4.8%), plus a non-byte-identical linker win of **−27%** on large +links. What landed, by segment: **intern** — inline `sym_eq`/miss-copy to drop +the freestanding `memcmp`/`memcpy` libcalls; **codegen** — a 1-byte `wide_kind` +tag cached on the value-stack node collapses the per-binop type-query gauntlet to +a load+compare, plus a flat `reg_last_use` regcache mirror; **parser** — fused +redefinition+define into one hash probe + O(1) keyword classification; **lexer** +— dropped the per-token `memset` + a splice-free fast-path scan; **pp** — +out-pointer token readers killing the 24B sret round-trip; **emit** — inlined +`buf_write` fast path + cached section `Buf*`; **abi** — right-sized the +`NativeCallPlanRet` scratch; **link** — HW-SHA image-id (the one non-byte-id +change: only the UUID/build-id bytes move). New tooling: +`scripts/perf_identity_gate.sh` (60-category byte-identical gate) and +`scripts/perf_axis_time.py` (focused A/B axis timer). The structural bet is fully in place: a single-pass no-AST C frontend, single-pass code emission with patch-ups, and a format-neutral linker. The scaling bugs are @@ -139,36 +157,51 @@ substitution that *is* `-E`'s work.) ## Forward-looking — the remaining levers -Ranked by (impact × confidence) / risk. All are the higher-risk -codegen/representation changes; codegen and the per-function `memset` are where -the compile-axis time now concentrates. - -1. **Per-operand type-query gauntlet** (codegen — biggest compile-axis lever). - `kit_cg_int_binop` runs `api_i128_stack_top`×2 + `api_wide64_stack_top`×2, - each a `cg_type_get` + `api_unalias_type` round-trip, on *every* int binop - only to detect a rare wide case. Those two are the top frames inside the - 27–42 % codegen bucket. Fix: compute a small `wide_kind` flag once when a - value is pushed and store it on the value node; the predicates become a - load+compare. Touches the rv64/i128 split paths → verify on `test-smoke-rv64`. -2. **`-O0` register-cache scan** (`nd_pick_cache_victim`, `nd_dst_reg`). A - per-pressured-op linear scan over the 16 allocable regs that re-derefs - `nd_local(owner)->last_use` with a bounds-check each iteration. Fix: a flat - `reg_last_use[cls][reg]` mirror so the scan reads one array. Victim *selection* - can change regalloc → gate on run-correctness, not byte-identity. -3. **Residual per-function `memset`** (still 8–16 %). The remaining explicit - zero-init is `aa_plan_ret`'s `arena_zarray(NativeCallPlanRet, 4)` (512 B/fn, - ≤1 used) and per-function arena scratch — right-size / lazily-zero them. -4. **Token-pump sret tax** (deepest; the pp-macro lever). `lex_next` / - `src_next_raw` return a 24-byte `Tok` by value, so each token round-trips - through an x8 indirect-result stack slot. An out-pointer on the hottest - readers (or shrinking `Tok` ≤ 16 B by packing `SrcLoc`) returns it in - registers — but `SrcLoc` is load-bearing frontend-wide, so bench-gate it in - isolation. +The Round-6 sprint took the constant-factor wins (the four levers previously +listed here — the type-query gauntlet, the regcache scan, the per-function +`memset`, and the token-pump sret tax — all landed). What remains is +**structural**: byte-identity now caps the compile axes at constant-factor +shaving, and **codegen is the dominant bucket** (27–42 % of every per-statement +axis). The remaining levers were each prototyped/measured (the gate for these is +run-correctness + determinism, not byte-identity, since they change output by +design). Ranked by (impact × confidence) / risk: + +1. **Copy-and-patch codegen** (the structural ceiling-raiser — biggest potential, + biggest effort). Precompiled per-IR-op machine-code stencils emitted by + `memcpy` + hole-patching, replacing the value-stack / regcache / type-query / + MC-emit path. A narrow aa64 spike (load/store/add/mov-imm/ret) measured + **~24× faster per op** (~175 ns/op → ~7 ns/op), cross-validated: the spike's + CG-region cost is 41 % of marginal whole-compile cost, matching the profiler's + 42 % codegen self-time. But whole-compile is **Amdahl-capped at ~1.66×** on the + most codegen-heavy axis (body-size) and less elsewhere — it cannot touch the + ~59 % frontend. A full backend is a large multi-arch investment (a stencil- + generation toolchain — harvest `.text`+relocs from compiled snippets — plus + reloc/ABI handling and variable-length x86 encoding). Worth it only as a + deliberate big bet on the dominant bucket. (Spike + numbers persisted under + `build/perf-nonbid-analysis/codegen-copypatch-spike.md`.) +2. **16-byte `Tok` (register return)** — *implemented and byte-identical* (a + lossless 32-bit `LocId` side-table replaces the inline 12 B `SrcLoc`, so + diagnostics stay exact and it passes the full byte-id gate), and it does + achieve the `x0:x1` register-return ABI (no x8 sret). But it measured + **neutral**: the two per-token `kit_loc_intern` calls it adds cost about as + much as the sret copy they remove. To flip it positive, **inline the loc-table + append into the lexer's hot loop** (publish the append cursor so a + static-inline fast path does one bounds-compare + store, out-of-line only on + grow). The clean 16 B `Tok` foundation is the prerequisite and is done. +3. **Keyword-id on the `Sym`** — a deeper, also-byte-identical variant of the + landed parser `KwMap`: it CSEs the ~30-call `is_kw` chain in `parse_decl_specs` + through one classification (the map alone leaves that chain doing per-`k` + probes). Measured ~10.5 % frontend-CPU on type-decl. A swap-in for the current + `KwMap` (they touch the same `ident_kw_inline`/`is_kw` lines, so only one + lands). **Deliberately not pursued** (measured dead ends, keep them dead): -image-id FNV→HW-SHA is a real symbol-count win but changes every executable's -LC_UUID/build-id bytes (needs a golden regen — separately gated); `nd_grow_*` -non-zeroing has an uninit-read risk no available sanitizer catches; a -word-at-a-time intern hash, a lower hash load factor, and an inline-prefix entry -cache all measured *slower* or negligible for the short identifiers real code -uses. The residual top frame on pathologically large links is the image-id FNV. +**fused lex→pp→parse** (a pull pipeline that never materializes a `Tok` array) — +measured: the pure pull-wrapper layers are only ~5 % of frontend self-time and +`pp_next_raw`'s cost is mostly macro/directive decision logic a pull pipeline +still runs, so the payoff doesn't justify the rewrite. **`nd_grow_*` non-zeroing** +has an uninit-read risk no available sanitizer catches. A **word-at-a-time intern +hash**, a **lower hash load factor**, and an **inline-prefix entry cache** all +measured *slower* or negligible for the short identifiers real code uses. +(Done since this list last named it: **image-id FNV→HW-SHA** — landed, −27 % on +large links; it was fenced off only for changing UUID/build-id bytes.)