commit 0b33acede42ae594f52d5ee544446332bc8aa772
parent bcdbeba368da56abbac093222d97d19298491466
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 09:47:15 -0700
doc(perf): PERF-IDEAL.md — touch-once/classify-once rewrite roadmap + WAVE 1-2 landings
Diffstat:
1 file changed, 416 insertions(+), 0 deletions(-)
diff --git a/doc/plan/PERF-IDEAL.md b/doc/plan/PERF-IDEAL.md
@@ -0,0 +1,416 @@
+# Performance by ideal flow — rewrite to the conservation law, don't patch around it
+
+Companion to `PERF.md` (standings + methodology), `PERF-TCC-GAP.md` (structural
+diagnosis), and `PERF-TCC-SLIM.md` (the patch-oriented slimming campaign). This
+doc reframes the whole effort around a single discipline:
+
+> **For each entity the compiler handles, name the work that should happen
+> _exactly once_ per entity — the conservation law / ideal flow — then rewrite the
+> subsystem to obey it, instead of memoizing or short-cutting around a structure
+> that re-does the work N times.**
+
+This is tcc's secret stated as invariants: *each source byte is touched ~once,
+each token is an `int`, each operator is a `vtop` adjustment plus ≤1 four-byte
+store, and there is no second copy of the program anywhere.* kit is ~3.1× tcc in
+**retired instructions** (the metric — load-independent, Linux callgrind `Ir`);
+kit's IPC already exceeds tcc's, so the gap is **instructions, not cache**. Each
+violated conservation law below _is_ a slice of that 3×.
+
+## One refinement up front: the objective is minimum *instructions*, not minimum *passes*
+
+"Touch each byte exactly once" is a proxy for "do the minimum work," and usually
+the two agree — but not always. The splice pre-scan (§A) is the case that
+separates them: the literal one-pass ideal (fuse `\<newline>` handling into the
+tokenizer, tcc-style) would reintroduce a per-byte splice test into the hot
+tokenizer loops that the raw-cursor rewrite deliberately made splice-free —
+**taxing every token to save a pass.** A separate SIMD-cheap pre-sweep (`memchr`,
+~1 instruction per 16–32 bytes) does the same job for fewer total instructions.
+So the law we optimize is **minimum retired instructions per entity**; "touch
+once" is how we reason about it, not a goal in itself. Where the two diverge,
+instructions win — and we say so explicitly.
+
+## The fresh profile (the evidence base)
+
+`kit cc -c sqlite3.c` (the real benchmark), callgrind at HEAD `de8c3b62`, **2.04 B
+`Ir` total**. Self-time buckets (trust self/exclusive — inclusive is garbage here
+because lex↔pp↔parse form one mutual-recursion cycle callgrind double-counts):
+
+| area | self % | conservation law it violates |
+|---|--:|---|
+| **types / CG-drive** | **20.7%** | each type decoded/classified once; all ancillary info cached on the id |
+| lexer scan (`lex_next`+`finish_ident`) | 16.3% | each byte classified once into one token |
+| pp pump + relay | 11.6% | a token lives in one place, never copied frame-to-frame |
+| **splice pre-scan** (`lex_open_mem`) | 10.6% | bytes not destined for a token are not walked by a scalar pass |
+| intern (`pool_intern_slice`) | 4.2% | each lexeme hashed once, then it is an int forever |
+| native emit (NDT path) | 3.9% | each machine byte written once into the final image |
+| other (parse/sema/objwrite long tail) | 26.3% | — |
+
+Artifacts: `build/linux-prof/cg.annot.txt` (flat self-`Ir`),
+`build/linux-prof/cg.tree.txt` (caller/callee + call counts). Recipe + gotchas:
+`PERF.md` §6. Every rewrite gates byte-identical
+(`scripts/perf_identity_gate.sh`) unless it deliberately changes emitted bytes
+(then: run-correctness + determinism).
+
+---
+
+## §A — Source bytes: *classify once; never scalar-walk a byte that isn't becoming a token*
+
+**Ideal flow.** A source byte is loaded, classified via `cclass[256]`, and
+consumed into exactly one token body — once. Bytes that exist only to be skipped
+(whitespace, the rare line-continuation) cost a table test, not a separate scalar
+pass.
+
+**Violation (10.6%, `lex_open_mem`).** Every opened buffer is walked **twice**.
+`lex_fold_splices` (`lang/cpp/lex/lex.c:149`, inlined into `lex_open_mem:281`)
+runs a scalar, 2×-unrolled byte loop (`lex.c:156-157`,
+`for(i;i+1<len;++i) if(src[i]=='\\' && src[i+1]=='\n') ++nspl;`) over the *entire*
+buffer to count `\<newline>` splices — which are almost always zero — and then
+`lex_next` walks every byte again to tokenize. Confirmed by disassembly of the
+profiled binary: the count loop is `ldurb;cmp #0x5c;ldrb;cmp #0xa;cset;...`,
+~4–5 instr/byte, **no NEON**. Across the ~3,140 file opens of the sqlite
+amalgamation's transitive header closure (`file_register → lex_open_mem`,
+`cg.tree.txt:56`) this is tens of millions of bytes scalar-walked for nothing.
+(`PERF.md:278-279`'s claim that the splice-free path "pays nothing" is true of
+`lex_next` during tokenization but **false for the open-time pre-scan** — the doc
+overlooked this counting pass.)
+
+**The rewrite.**
+1. **`memchr`-driven splice sweep** (the instruction-ideal — see the refinement
+ above). Replace the scalar count loop with `memchr(p,'\\',...)` to skip
+ non-backslash spans at glibc-NEON speed; backslash is rare in C, so the sweep
+ consumes the buffer at ~1 instruction per 16–32 bytes. Byte-identical (same
+ `nspl`, same fold table; the fold path is entered only when `nspl>0`).
+ **215M → ~20–40M `Ir` ≈ −8.5…9.5% of the whole compile.** This is the single
+ biggest lever in the profile and near-zero risk. The full tcc-style fuse is
+ explicitly **rejected**: it taxes the splice-free hot loops (`lex_next`,
+ `skip_ws_fast`, `scan_ident_run`, `scan_pp_number`, header-name scan) with a
+ per-byte test and would force spelling reconstruction for spliced tokens.
+2. **Delete the per-lexer `Pool`.** `lex_open_mem` mints `l->pool = c_pool_new(c)`
+ (`lex.c:288`) — a 64 B `Pool` + a `kit_arena_new` — on each of ~6,000 opens,
+ but the lexer touches `l->pool` only as `l->pool->c` (the Compiler) at three
+ sites, and `l->c` already holds it. Drop the field; replace `l->pool->c` →
+ `l->c`. **~20–30M `Ir`**, byte-identical, clean cutover. (Identifier interning
+ is unaffected — it already targets the shared `c->global` symtab via
+ `kit_sym_intern`, _not_ the per-lexer pool; the pool was dead weight.)
+3. **Hoist the punctuator-spelling cache to per-compile.** `struct Lexer` is
+ 1216 B, ~1184 of it `Sym punct_sym[]`, `memset`-zeroed per open and re-interned
+ cold each time — so the same ~60 punctuators are re-interned across ~6,000
+ lexers (the "classify once" law: a punctuator spelling should be interned
+ **once per compilation**, not once per lexer). Move `punct_sym[]` to a
+ per-compile `LexShared`; `Lexer` shrinks to ~32 B, its per-open `memset`
+ collapses, and punctuators are interned ~60 times total. **~5–15M `Ir`** +
+ relief on the 2% `memset`. More invasive (threads a shared ctx into
+ `lex_open_mem`); preserve the digraph-bypass and the "entry 0 = not-yet-seen"
+ sentinel. Do third.
+
+**Floor.** The irreducible work is one classified pass over each distinct
+buffer's bytes (the tokenize pass) + a near-free SIMD splice sweep. The rewrite
+reaches it. **Area est.: ~10–11% of the whole compile**, all byte-identical
+(gate + the line-splice battery: mid-token / mid-string / leading / trailing /
+consecutive continuations + a diagnostic line across a splice).
+
+---
+
+## §B — Identifiers: *each lexeme hashed once, then it is an integer forever*
+
+**Ideal flow.** The first time a spelling is scanned it is hashed → `Sym` (a small
+int); every later occurrence is *already* that int. Macro/keyword/symbol
+resolution is a load off the interned token, never a re-hash. (tcc: the token
+leaves the scanner as an int.)
+
+**Violation (4.2%, `pool_intern_slice`).** Every occurrence of every
+identifier/number re-runs FNV-1a + an open-addressed probe — ~628K calls ×
+~135 `Ir` — on a token stream with **22.4× reuse** (measured on real sqlite3.c:
+440,942 id+num occurrences, 19,700 distinct). The probe's `sym_eq`
+(`src/core/pool.h`) loads a hash-scattered `ents[sym]` line per occupied slot
+tested. *(Note: the interner is already per-compile-shared — `c->global`,
+`api/core.c:222`. The per-lexer `Pool` from `c_pool_new` is the **frontend** pool
+(arena + type/abi cache), a different type, not a string interner — so there is no
+per-buffer intern fragmentation to recover; the cache below belongs on
+`c->global`.)*
+
+**The rewrite — a direct-mapped, byte-verified intern cache inside
+`pool_intern_slice`.** An array of `{u32 keyhash; Sym sym}`, N a power of two,
+keyed on a *cheap* fold of `len` + the first ≤4 bytes (**not** full FNV). On
+lookup: index `= fold & (N-1)`; if the slot is occupied, **byte-verify** the
+candidate via `pool_slice(sym)` (the same compare the probe's `sym_eq` does) — on
+match return, having skipped FNV *and* the probe. On miss, fall through to the
+existing FNV+probe and populate the slot. Placing it inside `pool_intern_slice`
+(not the lexer) serves **frontend and codegen** interns alike — e.g. `aa_plan_call`
+re-interns ABI helper names (`__floatditf`, …) 19,455× — which a lexer-local cache
+would miss. Pure accelerator: every hit byte-verifies, so it can never return a
+wrong `Sym` (digraph-safe by construction — `<:` and `[` differ in bytes and fail
+verify); store only `{keyhash, sym}`, never a borrowed `in.s` pointer (it aims at
+transient lexer buffers).
+
+Measured hit rates on real sqlite3.c: **256-slot ≤4-byte-fold = 78.7%**, 1024-slot
+= 85.3%. This is explicitly **not** PERF-TCC-SLIM's A1 single "last-spelling" slot
+— that measured **3.4%** (real C alternates: `sqlite3 . pVfs -> xOpen (`), so a
+1-slot cache never hits. It is also distinct from the documented dead ends (the
+*entry-side* inline-prefix cache and the word-at-a-time hash, which don't remove
+the per-occurrence hash+probe): this removes both the hash and the probe for ~79%
+of calls.
+
+**Floor.** Steady state = one cache load + a ~6-byte verify per occurrence (~20–25
+`Ir`); one FNV+probe per distinct lexeme + collisions. At 78.7% × ~564K × ~110 `Ir`
+saved minus the cache's own overhead: **−30…45M (1.5–2.2%)** — the single biggest
+lever in this area. Byte-identical (gate + a digraph spelling check:
+`<: :> <% %> %: %:%:` must stay distinct).
+
+**Deferred — _not_ an `Ir` lever:** the self-sufficient `{hash, sym}` probe slot
+(PERF-TCC-SLIM A2 / `PERF-TCC-GAP` Track 3). Measured ~0 on instructions — it
+trades an `ents[sym].hash` load for a `slot.hash` load (same instruction, better
+locality), so it is a **D1-miss / wall-clock** play the `Ir` metric cannot see,
+and the cache above already absorbs ~79% of probes. Revisit only under a dedicated
+cache-stall investigation. *(This corrects PERF-TCC-SLIM's framing of A2 as an
+instruction lever.)*
+
+---
+
+## §C — Tokens: *produce only tokens the parser consumes — never build one that's always dropped*
+
+**Ideal flow.** The pump emits, into a fixed out-pointer slot, only tokens the
+parser will read; a token that is always discarded downstream is never built or
+relayed. The "lives in one place, never copied" half of the ideal is **already
+met** — and the work to do here is *removing a token class*, not restructuring the
+relay.
+
+**The copy half is already at the ideal (measured — relay rewrite is a confirmed
+dead end).** The lex→pp→parse relay already uses out-pointer (`_into`) forms
+(`pp.c:72`, `pp_expand.c:935`) precisely to kill inter-frame sret copies. The
+~206M `Ir` in `src_next_raw_into` / `pp_next_raw_into` / `fetch_tok` / `advance` is
+**per-token macro / directive / include-guard / `#line` logic, not copy
+bandwidth** — it survives any relay restructuring (`PERF.md`'s "fused pull pipeline
+= ~5% dead end", reconfirmed). Likewise **`Tok` 24→16 B is a dead end**: `loc.line`
+is read/written on the *hot* PP path (`#line` delta `pp.c:123`, `__LINE__`
+`pp_expand.c:1021`), so an inline-packed position forces a per-token decode that
+costs more than the copy it saves — the `LocId` side-table regression,
+reconfirmed.
+
+**The real violation — 51% of produced tokens are discarded.** `lex_next` is
+called **1,424,699×** but only **684,790** tokens reach the parser (`pp_next`):
+~740K (51%) are dropped inside PP, overwhelmingly `TOK_NEWLINE` — each built by
+`lex_next` (24 B), sret-relayed through `src_next_raw_into` + `pp_next_raw_into`,
+then `continue`'d in `pp_next`. `TOK_NEWLINE` cannot be removed outright (the
+directive scanner needs line boundaries: `#define` / `#if` / `read_directive_line`
+scan to it), but the non-directive ~95% need not be materialized as parser-facing
+`Tok`s.
+
+**The rewrite.**
+- **§C.3a (low risk):** absorb the newline-drop one frame lower so a non-directive
+ newline never pays the `pp_next` sret round-trip (`pp.c:191`,
+ `pp_expand.c:935`). **−5…12M.** Byte-identical (-E newline placement is the
+ critical gate).
+- **§C.3b (structural follow-up):** don't materialize non-directive newlines as
+ `Tok`s at all — the lexer sets `TF_AT_BOL` on the next content token (already
+ tracked) and emits an explicit end-of-line marker only *inside* directive
+ context. **−15…25M**, med-high risk: must prove every `TOK_NEWLINE` consumer is
+ directive-internal or expressible via `TF_AT_BOL` (`#if`/`#define`
+ line-termination is the hazard). Map the directive-scanner contract first.
+
+**Floor.** The pump's irreducible cost is the per-token macro/directive/guard logic
+over the *content* tokens only; newlines collapse to a flag on the next token.
+**Area est.: −5…12M now (3a), −15…25M follow-up (3b).** Gate: byte-identical -E +
+the directive corpus (`test/pp/`) + the line-splice battery.
+
+---
+
+## §D — Types: *decode/classify once; all ancillary info cached on the id*
+
+**Ideal flow.** At construction, a type is classified once and carries — on a
+stable id — its size, align, regclass, predicate bitset
+(`is_int/float/ptr/void/record/aggregate/i128`), unaliased id, lowered `CgId`, and
+record layout. Every later query is **one load**. There is one type
+representation the whole pipeline reads, not a `Type*`→`CgId`→`CgType` re-crossing
+per operand.
+
+**Violation (20.7% — the single largest area).** Predicates re-derive recursively
+through `cg_type_get` ↔ `api_unalias_type` ↔ `abi_cg_type_info`: `cg_type_get`
+recurses on itself **610K×**, `api_unalias_type` is called **~660K×**,
+`cg_type_is_aggregate` 31M self over ~550K calls — each re-decoding the id through
+a two-level segvec index. Root causes the prior "F1 memo" missed (it memoized
+`abi_cg_type_info`, which sat on already-flat field reads — hence its measured
+−2.1M):
+- **`c_abi_record_layout` is _not_ memoized and is rebuilt O(M·N)** (M member
+ accesses × N fields). `lang/c/abi/c_abi.c:85` `arena_znew`s a fresh layout and
+ loops every field calling `kit_cg_type_record_field` + `kit_cg_type_size` per
+ field — on every call. Its exact CG twin `abi_cg_record_layout`
+ (`src/abi/abi.c:150`) **is** memoized via `AbiRecLayoutMap`. *(Both verified.)*
+ This is the direct source of the `kit_cg_type_record_field` (1.39M calls) /
+ `kit_cg_type_size` (648K) cluster.
+- **The parser's value stack carries `const Type*`** (`cg_adapter.h:65`), so every
+ CG-driving op re-crosses the bridge via `type_cg_id_in_pool` (>1.2M calls), and
+ `type_cg_lower` runs a `t->kind` switch *before* checking its own `t->cg_id`
+ cache (`type.c:840-846`).
+- **Two parallel type-info memos** (`c_abi_type_info` keyed on `Type*`,
+ `abi_cg_type_info` keyed on `CgId`) wrap the same facts; a `sizeof` probes one,
+ crosses the bridge, then reads the other.
+
+**The rewrite.**
+1. **Memoize `c_abi_record_layout`** (highest leverage, lowest risk) — mirror the
+ `AbiInfoMap` pattern (`c_abi.c:22`) or, better, cache the layout pointer on the
+ `Type` node itself (it already carries a mutable `cg_id` slot, `type.h:94`).
+ The O(M·N) rebuild collapses to one build per distinct record. **−40…80M.**
+ Byte-identical.
+2. **A flat per-id descriptor** — extend the proven `cached_class` byte
+ (`type.c:33`, with its `pad[3]`) to a packed `{unalias_is_self, pred_bitset,
+ unaliased_id}` stamped once (eagerly at construction for builtins/records).
+ Predicates become builtin-fast-path OR one `api_type_from_id` + a byte test;
+ `api_unalias_type` returns `e->unaliased`. **−15…30M.** Byte-identical.
+3. **Carry the lowered `CgId` on the `PcgSlot`** alongside `const Type*`, filled
+ once at push — operands stop re-crossing the bridge. Plus hoist the `t->cg_id`
+ cache check above the `type_cg_lower` switch. **−15…30M** (P3a byte-identical;
+ P3b re-stamp `cg_id` whenever `slot->type` is written; couples with the
+ `PcgSlot`/`ApiSValue` shrink so net size is neutral).
+4. **Collapse `c_abi_type_info` into a thin reader** off the descriptor once (2)
+ and (3) land — delete the `AbiInfoMap` probe. **−8…14M.** Byte-identical.
+
+**Floor.** The irreducible work is one decode + classify + layout per *distinct*
+type, then O(1) loads. The rewrite reaches it. **Area est.: ~80…140M (−4…7%)** —
+the first credible double-digit-M win in this campaign, because (1) attacks genuine
+recomputation, not a constant factor. Sequence: (1) first and independent; (2) and
+(3a) independent; (4) after (2)+(3b). Re-profile after (1).
+
+---
+
+## §E — Operands & emit (the -O0 NDT path): *pop, pop, emit — one real dispatch, one byte-store*
+
+**Ideal flow.** An operand is a small fixed stack record; an operator is
+`pop,pop,emit` with exactly **one** indirect call (the real per-arch
+`NativeTarget` dispatch) and ≤1 four-byte store *into the final section image*. No
+marshalling round-trip, no fake vtable, no mini register-allocator on the hot
+path. The fold/strength-reduce ladder stays (it removes emitted bytes — the
+multiplicative codesize lever); only its *decision cost* is slimmed.
+
+**Already at the ideal (verified — do not re-propose).** `F2` (MCEmitter
+devirtualized to direct extern calls; the `static inline` form regressed +21M and
+is documented out-of-line), `D6` (relocations indexed by section — the
+O(n_sections × n_relocs) rescan is gone, the latent superlinear axis closed), and
+the value-stack residency + coalescing + lazy-home work (object now 1.09× tcc) are
+**done**.
+
+**Violation that remains.**
+- **A per-instruction libc `memcpy` in the byte sink.** `aa_emit32`
+ (`aa64/native.c:329`) stages a word into `u8 b[4]` then calls the out-of-line
+ `mc_emit_bytes(mc,b,4)` → `buf_write` → `memcpy(t->data+t->used, data, n)`
+ (`buf.h:55`). Because the call boundary blocks const-propagating `n=4`, the
+ memcpy lowers to a **libc call: 6.94M `Ir` over 434K calls** — one per emitted
+ word, the dominant memcpy in the whole compile. *(Verified.)*
+- **The arch re-queries types the NDT already knew.** `loc_is_64` /`type_size32`
+ (`aa64/native.c:374`) call `cg_type_size` + `cg_type_is_ptr` on `loc.type` for
+ every binop/move/convert/cmp — ~6.9M of `cg_type_size`'s callers are backend
+ re-derivation of a width the NDT had when it built the `NativeLoc`.
+- **A 48 B `NativeLoc` marshalling round-trip** — `nd_binop` passes 3×48 B by
+ value; the arch immediately unwraps each to a register number.
+
+**The rewrite.**
+1. **Typed-store byte sink** (the "F2 second half," never built): add direct
+ `mc_emit32(MCEmitter*, u32)` / `mc_emit_word(…, u64, n)` against a `{cur,end}`
+ cursor cached on the base — `if (cur+4<=end){ wr_u32_le(cur,word); cur+=4; }
+ else slow()`. `aa_emit32`/rv64 call it directly: **one 4-byte store, no
+ staging, no libc call.** **~5–6M**, compounds with codesize (per emitted word).
+ Risk: cursor↔`Buf::used` coherence across the slow path / `buf_patch` /
+ `mc_pos` (sync before any random-access read). Byte-identical + a `-g` check.
+2. **Stamp `is64`/`size_log2` on the `NativeLoc` pad** (`native_target.h:261` has
+ free `pad[2]`) at the `native_loc_reg` choke point; the arch reads the bits
+ instead of `loc_is_64`/`type_size32`. **~4–6M**, byte-identical, low risk
+ (falls back to `loc.type` where unset). The arch-side elimination is
+ independent of §D's descriptor.
+3. **Narrow register-only `NativeTarget` hooks** (`binop_rr` etc., ~16–24 B with
+ the imm rider vs 144 B/op) for the post-materialization ops; keep the fat
+ vtable for the opt replay path, gate on `opt_level==0`. **~3–5M**, additive
+ across all arches; synergistic with (2) (the hook carries exactly what the
+ arch reads). Byte-identical for the rr refactor; defer the x64 fused
+ mem-operand sub-case (changes selection → run-correctness gate).
+
+**Explicitly dropped** (measured/judged not worth it): D2 round-robin victim (the
+LRU is inlined, invisible to `Ir`, and coarser eviction risks the codesize track —
+instead a policy-neutral free-register bitmap for the `nd_cache_alloc` free-pool
+scan is ~1–2M, byte-identical); C3 `ApiSValue` shrink (Round-7-neutral;
+`api_sv_adjust_refs` at 0.67% is the load-bearing liveness refcount that *drives*
+coalescing, not copy bandwidth); D5 zero-copy section write (`buf_flatten` is only
+0.25M `Ir` — invisible to the metric).
+
+**Floor.** This subsystem's micro-levers each land single-digit-M (as
+`PERF-TCC-SLIM` §10b warned); items 1–3 + the bitmap total **~13–19M (−0.6…0.9%)**.
+The real emit-phase lever is the **codesize track** (fewer emitted bytes →
+multiplicatively less byte-sink/objwrite/assemble), already most-realized by the
+value-stack work. Item 1 leads because it compounds with codesize. **Do not touch
+the fold ladder** (`arith.c:48-99`) — `PERF-TCC-SLIM` §8 holds.
+
+---
+
+## §F — Emitted bytes / objwrite: *each machine byte written once into the final image*
+
+**Ideal flow.** Machine code is built once in the final section buffer; symbols,
+strings, relocations are each bucketed once. No flatten-to-temp second copy, no
+O(n²) rescan.
+
+**Status.** The two historical O(n²) violations are **closed**: `obj_strtab_add`
+(shared content-hashed `ObjStrtab`) and the reloc-by-section rescan (`D6`,
+`obj_reloc_index_ensure`, a counting sort — verified). The residual `buf_flatten`
+second copy of all machine code at ELF/Mach-O write is real but only ~0.25M `Ir`
+(one big 2.2 MB copy, cheap per byte) — **below the metric's noise; not worth the
+Writer-API surface change** unless a future wall-clock/cache pass resurfaces it.
+The full tcc-shape ideal (`mc_emit32` stores into one contiguous geometric section
+buffer = the final image, no chunked `Buf` at all) is the §E-item-1 cursor taken
+to its conclusion; revisit only if §E-1's chunked cursor still shows the spill
+path hot.
+
+---
+
+## Sequencing by leverage (re-profile after each — the frontier moves)
+
+```
+WAVE 1 (biggest, cleanest, all byte-identical):
+ §A.1 memchr splice sweep ~ -8.5…9.5% (largest single lever)
+ §D.1 memoize c_abi_record_layout ~ -40…80M (the F1-missed recomputation)
+ §B.1 direct-mapped intern cache ~ -30…45M (79% of interns are redundant)
+
+WAVE 2 (high-leverage, byte-identical):
+ §A.2 delete per-lexer (frontend) Pool ~ -20…30M
+ §D.2 flat per-id type descriptor ~ -15…30M
+ §D.3 CgId on PcgSlot + cg_id hoist ~ -15…30M
+ §E.1 typed-store byte sink ~ -5…6M
+ §E.2 is64/size_log2 on NativeLoc ~ -4…6M
+ §C.3a newline drop one frame lower ~ -5…12M
+
+WAVE 3 (more invasive / structural follow-ups):
+ §A.3 per-compile punct table ~ -5…15M
+ §D.4 collapse c_abi_type_info ~ -8…14M
+ §E.3 narrow register-only NT hooks ~ -3…5M
+ §C.3b non-directive newlines never Tok ~ -15…25M
+
+DROPPED / not Ir levers (measured — do not pursue):
+ §B self-sufficient {hash,sym} slot (~0 Ir, D1-only) · relay-copy rewrite (dead) ·
+ Tok 24→16B (hot-path line decode) · §E D2 round-robin / C3 ApiSValue / D5 zero-copy
+```
+
+**WAVE 1 alone is ~13–15% off the 2.04 B compile** (moving kit from ~3.1× toward
+~2.7× tcc) for three low-risk, byte-identical rewrites. All three attack **genuine
+recomputation** — an unmemoized O(M·N) layout rebuild (§D.1), a redundant scalar
+pass over every byte (§A.1), and 79%-redundant per-occurrence hashing (§B.1) — not
+constant factors. Those are the rewrites-to-the-ideal; WAVE 2–3 compound. Re-profile
+after each landing — the frontier moves, and several WAVE-2 items shrink once their
+WAVE-1 neighbor lands.
+
+## Appendix — claims verified against source (not docs)
+
+- §A scalar splice loop + no NEON: `lex.c:156-157`, disasm of the profiled binary. ✓
+- §A per-lexer pool dead except `->c`: `lex.c:288,340,597,784`; ident intern hits
+ shared `c->global` via `kit_sym_intern` (`src/api/core.c:220`). ✓
+- §B two distinct `Pool` types — `c_pool_new` makes the **frontend** pool (arena +
+ type/abi cache, `cpp_support.h:33`), *not* the string interner (`c->global`,
+ `src/core/pool.h`); intern is already per-compile-shared. ✓
+- §B/§C measured on real sqlite3.c (re-tokenized): 440,942 id+num occurrences /
+ 19,700 distinct (22.4× reuse); 256-slot intern cache 78.7% hit, single-slot 3.4%;
+ `lex_next` 1,424,699 calls vs 684,790 tokens to the parser (51% dropped in PP). ✓
+- §D `c_abi_record_layout` unmemoized vs `abi_cg_record_layout` memoized:
+ `lang/c/abi/c_abi.c:85-118` (fresh `arena_znew` + per-field loop, no cache) vs
+ `src/abi/abi.c:150-161` (`AbiRecLayoutMap_get/set`). ✓
+- §D parser stack carries `Type*`: `lang/c/parse/cg_adapter.h:65`. ✓
+- §E byte-sink libc memcpy: `aa64/native.c:329` (`b[4]`+`mc_emit_bytes`) →
+ `mc.c:242` (`buf_write`) → `buf.h:55` (`memcpy(...,n)`), n=4 not const-propagated
+ across the out-of-line call. ✓
+- §E F2/D6 already landed: `mc.h:48-82` (no fn-ptrs), `obj.c:1140-1196`
+ (`obj_reloc_index_ensure` counting sort). ✓