kit

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

commit 0bbb3b5f5425ee6015266f19c6b21dd092f6228c
parent cd997d0fecd8f80e5c141064ec4c1c4b5ff2f26a
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 22:59:37 -0700

docs(perf): record Round 5 (per-segment structural pass) + add hotspot sampler

Round 5 documents the cross-segment optimization pass (types/codegen/emit/link/
pp/lexer/core) and the methodology: hotspot-driven, byte-identical-gated, with
the auto-init experiment that proved memset is explicit (not -ftrivial-auto-var-
init). Adds scripts/cc_bench_hot.sh — drives one axis at a tuned large size so
macOS `sample` captures a real call graph now that the normal sweep runs sub-
400ms everywhere.

Diffstat:
Mdoc/plan/PERF.md | 44++++++++++++++++++++++++++++++++++++++++++++
Ascripts/cc_bench_hot.sh | 43+++++++++++++++++++++++++++++++++++++++++++
2 files changed, 87 insertions(+), 0 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -36,6 +36,7 @@ to the shipped release, so timings are representative) and runs |------|------| | `scripts/cc_bench_gen.py` | Axis catalog + synthetic source/object generator. `--list` prints the axes; `--axis/--n/--out` writes an instance + JSON manifest. **All axis definitions live here.** | | `scripts/cc_bench.sh` | Harness: build/locate kit, measure overhead, sweep, time kit + clang, correctness-check, sample. Writes `scaling.csv`. | +| `scripts/cc_bench_hot.sh` | Hotspot sampler: drives one axis at a time at a size tuned for a ~1.5–3 s run (the normal sweep is now too fast to sample), so `sample` captures a real call graph. Writes `build/bench/hot/<axis>/`. | | `scripts/cc_bench_report.py` | Exponent fit + verdicts + clang ratios → `scaling.md`; parses `sample` call-trees → `hotspots.md`. | | `mk/maint.mk: bench-cc` | Builds the `PROFILE=1` kit and runs the harness. | @@ -75,6 +76,49 @@ Quick wire-check: `KIT_CC_BENCH_SIZES='8 16 32' KIT_CC_BENCH_SAMPLE=0 make bench ## Findings +### Round 5 — per-segment structural pass (lexer / pp / parser-types / cg / emit / link) + +> 2026-06-10, M1, clang-built `PROFILE=1` kit. A measurement-driven sweep across +> every segment, grounded in fresh hotspots (multi-second runs via +> `scripts/cc_bench_hot.sh`, which drives one axis at a tuned large size so +> macOS `sample` captures a real call graph — the normal sweep is now too fast +> to sample). A decisive experiment first: rebuilding with +> `-ftrivial-auto-var-init=zero` **off** left `memset` self-time unchanged, so +> the pervasive #1 cost is *explicit* zero-init, attacked at the call sites (the +> hardening flag stays). Every change is object/`-E`/`-g`/diagnostic +> byte-identical to HEAD (the gate, since ASan can't see a premature-reuse or +> uninit read) except the two genuine latent O(n²) fixes, which only change +> *timing*. Headline (original HEAD → after, same machine + sizes): + +| segment | change | result | +|---|---|---| +| **types** | hash-cons structural types (ptr/array/func) — kills an O(types) per-derivation scan of one flat list; memoize ABI facts + record layouts | type-decl 1229→976 ms (−21%); record-heavy TU 904→41 ms (**22×**, was O(records²)); pointer-population stress 29.5 s→0.27 s (**108×**) | +| **codegen** | build `NativeLoc`/`NativeAddr`/`MemAccess` via designated compound literals, not `memset`+stores (the ~64 B descriptor zeroing was the top self-time on cg-heavy axes) | body-size −8%, locals −8%, ref-density −4% | +| **emit** | tail-chunk fast path in the chunked buffer; batch the aa64 prologue patch + 32-NOP reserve into one op each (x64 already did); skip per-instruction `obj_pos` without `-g` | fn-count 1056→752 ms (−29%) | +| **link** | per-section delta table + a `link_sec_id→MSec*` index replace two per-section rescans of the whole reloc+sym vectors in the Mach-O finalizer | obj-count restored to LINEAR; 16384 objs 1045→379 ms (2.76×, gap widens with N) | +| **pp** | drop the degenerate per-token hideset vector; batch `-E` output into a local buffer; pre-size the substitution token vectors | pp-macro `-E` 1316→985 ms (−25%) | +| **lexer** | **redesign**: fold translation-phase-2 line splices out once up front so the scanner walks a clean buffer with plain direct indexing — no per-byte splice re-test in `peek`/`bump`; splice-free input is zero-copy, spliced input keeps exact line/col via a fold-offset array | body-size −7%, type-decl −9%, pp-macro −6%, fn-count −4.5% on top of the above | +| **core** | hoist the table/entries bases out of the per-identifier intern probe | small, free, byte-identical | + +Net kit/clang `-O0` (lower = faster, all axes LINEAR): fn-count 0.12→0.08×, +body-size 0.33→0.27×, **type-decl 0.68→0.55×** (was the weakest compile axis), +pp-macro 0.48→0.37×, global-decl 0.42→0.34×, ref-density 0.08→0.07×, obj-count +0.26→0.23×. pp-include (file-I/O bound) and symbol-count (image-byte bound) +unchanged. + +**Consistency cleanup (per review):** added `KIT_HASHSET_DEFINE` (open-addressed +set with custom hash+eq) to `kit/support/hashmap.h` for the structural type +intern, routed the ABI/record memos through the existing `KIT_HASHMAP_DEFINE`, +and gave `Pool` one shared arena-heap facade (`Pool.arena_heap`) so all per-pool +maps — including the parser's scope/tag/extern indexes, migrated off their +duplicate local facade — get arena lifetime with no teardown. + +**Deliberately deferred.** image-id FNV→HW-SHA (a real symbol-count win but +changes every executable's LC_UUID/build-id bytes); `nd_grow_*` non-zeroing +(uninit-read risk without MSan); word-at-a-time intern hash / lower load factor / +inline-prefix cache (all measured *slower* or negligible for real short +identifiers). The residual top frame on the largest links is the image-id FNV. + ### Round 4 — CPU work: linker output hashing + the codegen type-query path > 2026-06-10, M1, clang-built `PROFILE=1` kit. With memory + IO handled diff --git a/scripts/cc_bench_hot.sh b/scripts/cc_bench_hot.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Targeted hotspot sampler: drive cc_bench.sh one axis at a time at a size tuned +# for a ~1.5-3s run, so macOS `sample` captures a reliable call graph (it needs +# >=1s). The normal sweep tops out under 400ms now (Rounds 1-4), so sampling is +# skipped there. Output: build/bench/hot/<axis>/{raw/<axis>.sample.txt,scaling.*}. +# +# Usage: KIT=build/bench/kit bash scripts/cc_bench_hot.sh [axis ...] +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +KIT="${KIT:-$ROOT/build/bench/kit}" + +# Per-axis single big size tuned for ~1.5-3s at the per-unit ns measured in the +# normal sweep. Two points where cheap so the reporter can still fit an exponent. +declare -A SIZES=( + [fn-count]="256000 512000" + [body-size]="512000 1024000" + [global-decl]="1024000 2048000" + [type-decl]="512000 1024000" + [locals-per-fn]="512000 1024000" + [pp-macro]="512000 1024000" + [pp-include]="64000 128000" + [ref-density]="256000 512000" + [symbol-count]="1024000 2048000" + [obj-count]="4096 8192" +) + +AXES="${*:-fn-count body-size global-decl type-decl locals-per-fn pp-macro pp-include ref-density symbol-count obj-count}" +for axis in $AXES; do + sizes="${SIZES[$axis]:-}" + [ -z "$sizes" ] && { echo "skip unknown axis $axis"; continue; } + echo "===== hot: $axis sizes=$sizes =====" + KIT="$KIT" \ + KIT_CC_BENCH_OUT="$ROOT/build/bench/hot/$axis" \ + KIT_CC_BENCH_AXES="$axis" \ + KIT_CC_BENCH_SIZES="$sizes" \ + KIT_CC_BENCH_REPEATS=2 \ + KIT_CC_BENCH_SKIP_CLANG=1 \ + KIT_CC_BENCH_MAX_MS=0 \ + KIT_CC_BENCH_SAMPLE=1 \ + KIT_CC_BENCH_SAMPLE_MIN_MS=800 \ + bash "$ROOT/scripts/cc_bench.sh" +done +echo "===== hot sampling done ====="