kit

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

commit cb91bcba2cc4a3ceedd6bf84e636bee09c9acf37
parent 1dcf1752ceb58c302140831ee751206d26e9959d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 07:28:24 -0700

doc(perf): rerun the sqlite profile after value-stack residency

Phase decomposition (best-of-7): -E 0.974B, -fsyntax 1.842B, -c 2.135B; tcc -c
0.664B. emit+objwrite (-c − -fsyntax) 0.37 → 0.29B, ~7× → ~5.9× tcc; total ratio
3.35× → 3.22×. 80-run hotspot sample: lex_next 71.9%, pool_intern_slice 9.7%,
finish_ident 6.1%, memset 6.1% — codegen dropped off the profile (<0.5%) after
the round-trips were removed. Updated standings, phase numbers, hotspot table,
the instruction-attribution callout, and Track 1 (round-trip half now done).

Diffstat:
Mdoc/plan/PERF.md | 78+++++++++++++++++++++++++++++++++++++++++++++---------------------------------
1 file changed, 45 insertions(+), 33 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -100,12 +100,14 @@ m build/release/kit cc -c -o /tmp/k.o sqlite3.c --sysroot "$SDK" # + codegen `-fsyntax-only` still drives the full CG value-stack and type lowering (routed to the no-op check backend), so `(-c) − (-fsyntax-only)` isolates **native emit + -object write** only. Current binary: `-E` 0.98 B, `-fsyntax-only` 1.85 B, `-c` -2.22 B → that delta is ~0.37 B / ~17 % — i.e. **codegen+emit is a small slice; -the frontend is the rest.** Within that frontend, `-E` alone (0.98 B) is just +object write** only. Current binary: `-E` 0.97 B, `-fsyntax-only` 1.84 B, `-c` +2.13 B → that delta is ~0.29 B / ~14 % — i.e. **codegen+emit is a small slice; +the frontend is the rest.** Within that frontend, `-E` alone (0.97 B) is just under half, and the scanner (`lex_next`) is the bulk of it — see *Where the time -goes*. (The scanner rewrite below cut a flat ~84 M instructions off every phase: -`-E` 1.06 → 0.98 B / −8.0 %, `-c` 2.30 → 2.22 B / −3.6 %.) +goes*. (Two landings shrank the emit slice: the scanner rewrite cut a flat ~84 M +off every phase — `-E` 1.06 → 0.98 B / −8.0 % — and the -O0 value-stack residency +work took emit+objwrite 0.37 → 0.29 B / ~7× → ~5.9× tcc by killing the call / +return / assignment home round-trips. See PERF-TCC-SLIM.md §10c.) **4. Hotspot profile** (self-time per function). One run is far too fast for `sample` (a `-c` is ~0.2 s now), so merge the `Sort by top of stack` sections @@ -139,33 +141,38 @@ build/release/kit cc sqlite3.c shell.c -o /tmp/sq --sysroot "$SDK" -lc ## Current state **Real-world compilation is frontend-bound, not codegen-bound.** The phase split -(instructions: `-E` 0.98 B, `-c` 2.22 B) puts native codegen + emit + object-write -at ~17 %; the frontend — preprocessor, lexer, interner, parser, semantic analysis, +(instructions: `-E` 0.97 B, `-c` 2.13 B) puts native codegen + emit + object-write +at ~14 %; the frontend — preprocessor, lexer, interner, parser, semantic analysis, and the CG value-stack/type-lowering it drives — is the rest. (The synthetic `bench-cc` axes below over-weight codegen by construction; trust the sqlite profile for where to spend effort.) ### Where the time goes (self-time, real sqlite `-c`) -80-run merged `sample` on a `PROFILE=1` build (1467 samples), **after the -raw-cursor scanner rewrite**. The profile is **single-peaked**: the scanner -still dominates and everything downstream of it is flat. (`sample` is +80-run merged `sample` on a `PROFILE=1` build, **after the raw-cursor scanner +rewrite and the -O0 value-stack residency work**. The profile is **single-peaked**: +the scanner dominates and everything downstream of it is flat. (`sample` is wall-clock self-time, which tracks the instruction metric for CPU-bound code -but *not* for blocking I/O — see the `fsync` row.) +but *not* for blocking I/O.) | function | self % | stage | note | |---|--:|---|---| -| `lex_next` | **62.2** | lexer (the scanner) | dispatch + ws/comment skip + number/string/punct scan + the per-token intern calls (`scan_pp_number`/`scan_quoted`/`skip_ws_fast` inlined in) | -| `pool_intern_slice` | 14.4 | identifier interning | one per ident/number; puncts cached | -| `_platform_memset` | 8.2 | zeroing | callers are **parse + codegen**, not the lexer | -| `pp_next_raw_into` | 5.0 | preprocessor token pump | | -| `finish_ident` | 3.7 | lexer (identifier scan) | part of the scanner lever (`scan_ident_run` inlined in) | -| `src_next_raw_into` | 3.0 | preprocessor source stack | | -| `fsync` | 1.8 | object write | flushing the `.o` to disk — **wall-time I/O, ≈0 retired instructions** (irrelevant to the instruction metric) | -| codegen (`nd_dst_reg`/`cg_type_is_aggregate`/`api_unalias_type`/…) | ~1.6 total | native emit | each <1 % | -| `_platform_memmove` | 0.3 | buffer moves | callers parse/codegen | - -The scanner (`lex_next` + `finish_ident`) is **~66 %** of *wall-clock* self-time. +| `lex_next` | **71.9** | lexer (the scanner) | dispatch + ws/comment skip + number/string/punct scan + the per-token intern calls (`scan_pp_number`/`scan_quoted`/`skip_ws_fast` inlined in) | +| `pool_intern_slice` | 9.7 | identifier interning | one per ident/number; puncts cached | +| `finish_ident` | 6.1 | lexer (identifier scan) | part of the scanner lever (`scan_ident_run` inlined in) | +| `_platform_memset` | 6.1 | zeroing | callers are **parse + codegen**, not the lexer | +| `src_next_raw_into` | 2.6 | preprocessor source stack | | +| `pp_next_raw_into` | 2.6 | preprocessor token pump | | +| `api_unalias_type` | 0.5 | types | the only codegen-side row still visible | +| `_platform_memmove` | 0.5 | buffer moves | callers parse/codegen | + +The scanner (`lex_next` + `finish_ident`) is now **~78 %** of *wall-clock* +self-time — up from ~66 %, because the value-stack residency work removed the +codegen/emit self-time, not because the scanner got slower. **Codegen has dropped +off this profile entirely** (the former ~1.6 % cluster — `nd_dst_reg`/ +`cg_type_is_aggregate`/… — is now <0.5 %), confirming the emit-phase shrink the +instruction decomposition shows (0.37 → 0.29 B). + Three things that used to be hot remain absent: the **type system** (derived-type + ABI/record dedup are O(1)), the **preprocessor hideset** (O(1) content-addressed dedup), and **guarded-header re-lexing** (multiple-include @@ -176,11 +183,12 @@ optimization). > `sample` profile makes the scanner *look* like the lone frontier, but it is a > wall-clock artifact of one big inlined leaf on a single-peaked workload. The > **instruction-grounded** decomposition (load-independent, the metric to trust) -> tells a different story: `-E` 0.97 B, post-PP 1.24 B, with the gap to tcc +> tells a different story: `-E` 0.97 B, post-PP 1.16 B, with the gap to tcc > spread **~uniformly ~3× across lex+pp / parse+sema+CG / emit** — and kit `-E` > already *beats* `tcc -E`. The highest-leverage single track is **codegen > density** (the CgTarget→NativeTarget→MCEmitter vtable stack + the -O0 -> value/RA machinery, ~7× tcc), not the scanner. kit's IPC (3.70) is *higher* +> value/RA machinery; the home-round-trip half is now done, ~7× → ~5.9× tcc), +> not the scanner. kit's IPC (3.70) is *higher* > than tcc's (3.48): the gap is **instructions, not cache stalls**. Full map, > per-phase diagnosis, and the redesign tracks: **`doc/plan/PERF-TCC-GAP.md`**. @@ -217,15 +225,19 @@ The instruction-grounded decomposition (above callout) puts the tcc gap structural leverage. Full per-phase diagnosis + redesign plan in **`doc/plan/PERF-TCC-GAP.md`**. -1. **Codegen density — collapse the -O0 emit stack** (Track 1, ~7× tcc, the - worst ratio). Three stacked vtables (`CgTarget`→`NativeTarget`→`MCEmitter`, - ~7–9 indirects per statement), the NDT LRU register-cache running as a - mini-RA at -O0, the fold/strength-reduce ladder on the -O0 hot path, the - uncached recursive `abi_cg_type_info`, and the 56 B `ApiSValue`. Fuse the two - native vtables into one direct emit path, memoize size/align/regclass on the - type id, gate the fold ladder off at -O0, shrink the value node. Highest - single lever; couples with Track B (`o0-codesize`: fewer emitted bytes → - less emit/objwrite). +1. **Codegen density — collapse the -O0 emit stack** (Track 1, now ~5.9× tcc + after the value-stack residency landing, still the worst ratio). The + round-trip half of this track is **done** — the call/return/assignment home + round-trips are gone (register-resident values + spill-only-live-set; see + PERF-TCC-SLIM.md §10c), which is why codegen has dropped off the hotspot + profile. What remains: the three stacked vtables (`CgTarget`→`NativeTarget`→ + `MCEmitter`, ~7–9 indirects per statement), the NDT LRU register-cache victim + policy (a mini-RA at -O0), the fold/strength-reduce ladder on the -O0 hot + path, the uncached recursive `abi_cg_type_info`, and the 56 B `ApiSValue`. + Fuse the two native vtables into one direct emit path, memoize + size/align/regclass on the type id, slim the fold-ladder *decision* (keep its + byte-producing effect — see SLIM §8), shrink the value node. Couples with + Track B (`o0-codesize`: fewer emitted bytes → less emit/objwrite). 2. **Identifier interning** (`pool_intern_slice`, ~14 %). Per-occurrence re-hash+probe of hot identifiers; cache/defer the intern, and make the probe slot self-sufficient (`hash`+`len`+SSO inline) so it rejects without the