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:
| M | doc/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