commit f617d98f6d820de190abcc74d1a07d1ceb0ee19f
parent 40e5f8644999c4411aa4d12b39958e1083c6fa1b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 12:09:47 -0700
doc(perf): PERF-IDEAL.md — §R-A/§R-B/§R-C worked in full, measured -1.86%
8 byte-identical items landed (A1/A2/A3/A5/B1/B2/B3/C4, 4 parallel worktree tracks),
5 skip-justified with the diagnosis verified against current source (A4 not
byte-identical-achievable; A6 ~0 Ir; C1 doc design was a latent rv64-fp miscompile;
C2 already subsumed by E.1; C3 sub-noise). sqlite3.c -c: 1,785,778,688 ->
1,752,564,544 Ir (-33,214,144, -1.86%), 2.69x -> 2.64x tcc; cumulative from
de8c3b62 -14.04%. Full 60-category byte-identity gate PASS, object bit-identical.
Diffstat:
1 file changed, 81 insertions(+), 0 deletions(-)
diff --git a/doc/plan/PERF-IDEAL.md b/doc/plan/PERF-IDEAL.md
@@ -479,6 +479,87 @@ the last 2.69× → ~2× needs the **codesize track** (fewer emitted bytes, the
multiplicative lever) and/or Track 4 (tcc's shared-token single-pass shape), not
more per-op slimming.
+## Measured results — §R-A/§R-B/§R-C implemented (2026-06-13, 8 commits `209596b8`..`fe54567a`)
+
+The seam-preserving backlog below was worked **in full**: 8 byte-identical items
+landed (4 parallel isolated-worktree tracks PP/PARSE/TYPE/OBJ, each per-item
+byte-identity-gated against a shared golden) and 5 items were **skip-justified with
+the diagnosis verified against current source** (they are not byte-identical-
+achievable, are ~0 `Ir`, are already done, or — for one — the doc's literal design
+was a latent miscompile). The "no per-op lever left" pessimism above was too strong:
+the seam-preserving per-op work still found **−1.86%** before the codesize track.
+
+**Cumulative, Linux callgrind, sqlite3.c `-c`:**
+
+| | Ir | vs tcc (0.663 B) |
+|---|--:|--:|
+| after WAVE 3 (`b50e883e`) | 1,785,778,688 | 2.69× |
+| **after §R landing (`fe54567a`)** | **1,752,564,544** | **2.64×** |
+| §R delta | **−33,214,144 (−1.86%)** | |
+| **cumulative (from `de8c3b62`)** | **−286,280,998 (−14.04%)** | **3.08× → 2.64×** |
+
+Gate: full 60-category byte-identity **PASS**; sqlite `-O0` object still bit-identical
+(2,216,680 B). Green: pp / parse / toy (1392/0/35skip) / cg-api / opt / elf / macho /
+link / ar / debug / dwarf / smoke-x64 / smoke-rv64.
+
+**Landed (8 items, all byte-identical):**
+
+| item | commit | mechanism |
+|---|---|---|
+| A1 Sym-keyed binding cache | `5ff33ea3` | `scope_lookup` → one `BindingTab_get`; shadow saved on `SymEntry.shadowed`, head-first unwind in `scope_pop` |
+| A3 macro body pointer-replay | `7ada3f62` | no-`##` object bodies replayed by pointer (`has_paste` cache + `TokSrc` loc/flag override + `push_buf_replay`); 2 copies/expansion removed |
+| A2 paste-path slimming | `39c384b1` | reuse one per-`Pp` paste lexer (`lex_reset_mem`) + cache the `<paste>` `Sym`; **file_id ordering preserved** (DWARF gate is the proof) |
+| A5 skip no-op `arena_reset` | `209596b8` | `arena_is_empty` pristine predicate + early-out + `pp_pull_into` guard |
+| B1 one decode per id | `9a060c6c` | CG-internal `api_type_pred_bits` (one decode → full bitset) at multi-predicate sites + hoist non-CSE-able `api_unalias_type` |
+| B2 fold dual ABI memo | `9bd45ea5` | delete `c_abi_type_info` + its `Type*`-keyed `AbiInfoMap`; size/align via the per-id memo; `signed_` → `type_is_signed_integer` |
+| B3 type-lower micro | `407e8ac9` | field-init `TypeCgLower` (drop `memset`) + stamp the builtin `cg_id` on the `Type` node |
+| C4 objwrite mop-up | `fe54567a` | single-pass Mach-O/ELF symtab counts + direct Mach-O strtab write (no flatten copy) |
+
+**Per-item attribution caveat (PERF.md §6).** The naive per-function self-`Ir` diff
+between the two builds **over-counts** wildly (its group-sum is −125M vs the real
+−33.2M `PROGRAM TOTALS`) because each item moves work into a *new* function the diff
+double-counts: `api_type_pred` 61.85M → `api_type_pred_bits` 48.83M (B1), `lex_open_mem`
+self collapses into `lex_reset_mem`+callees (A2), `arena_reset` 11.81M→0.44M but a new
+`arena_is_empty` +7.3M (A5), `subst_phase2` −4.07M but a new `push_buf_replay` +2.09M
+(A3), `c_abi_type_info` 19.13M removed but redistributed to the cheaper per-id memo (B2).
+**Trust the −33.2M total.** The one clean isolation is **A1 = net −6.8M** (`scope_lookup`
+8.91M→0.99M, bookkeeping +1.11M — its work stays inside the four scope functions), which
+**refutes the doc's worry** that the per-scope `emap` indexes already amortized it:
+`scope_lookup` was a real 8.9M chain-walk, and tcc's binding-on-the-symbol technique cut it.
+
+**Skip-justified — verified against current source, NOT implemented:**
+
+- **§R-A A4** (stop materializing non-directive newlines) — **not byte-identical-
+ achievable.** `lex_next` is the single source funnel feeding the directive scanner,
+ the macro-arg scanner, the `-E` emitter, and the cc content path on ONE shared
+ `Lexer`, interleaved dynamically; the "is this newline directive-internal / whitespace"
+ decision lives in PP, invisible to the lexer. `-E` requires 1:1 `TOK_NEWLINE`→`\n`
+ (forecloses collapse); directive-line termination and multi-line-macro accounting need
+ the materialized newline *in cc mode*. The §C.3a/b in-place drain (`bfa30f77`) already
+ captured the only safe win — the parser already never sees a newline `Tok`, so there is
+ nothing further to delete.
+- **§R-A A6** (intern probe/insert side) — **~0 `Ir`.** The 1024-slot direct-mapped intern
+ cache already absorbs ~85% of probes; the only candidate (the self-sufficient `{hash,sym}`
+ fat-table slot) trades one `ents[sym].hash` load for one `slot.hash` load — same
+ instruction, better D1 locality — a cache-stall play the `Ir` metric cannot see, and it
+ doubles the table. Revisit only under a dedicated cachegrind (D1mr/DLmr) study.
+- **§R-C C1** (free-register bitmap) — **the doc's literal design is a latent rv64-fp
+ miscompile.** `nd_cache_alloc`'s scan is `(reg_owner==NONE) && !pinned` over `cache_pool`
+ in **priority** order, not `caller_saved & ~owned` in register-number order; rv64-fp's
+ pool `{4,5,6,7,28,29,30,31,10,…}` is non-ascending, so register-number find-first-set
+ picks a *different* register than the linear scan (reg10 vs reg28) → different emitted
+ bytes, invisible to the default host gate. The only byte-identical variant (a priority-
+ position-indexed bitmap) lands its maintenance on hotter paths than the scan it removes →
+ a wash; and the scan already exits at index 0–1 (the live cached set is tiny after every
+ barrier flush).
+- **§R-C C2** (typed-store for memory-op emit) — **already subsumed by §E.1.** `aa_emit_mem`
+ already routes every word through `aa_emit32`→`mc_emit32` (the typed `{cur,end}` cursor);
+ there is no residual stage-bytes + `mc_emit_bytes` fixed-width path left to convert.
+- **§R-C C3** (contiguous section image) — **too invasive for the payoff.** `buf_flatten`
+ is ~0.25M `Ir` (one big cheap copy, below the metric's noise); replacing the chunked
+ geometric `Buf` with a single image `mc_emit32` writes into is a Writer-API surface change
+ for a sub-noise win. Revisit only under a wall-clock/cache pass.
+
## Remaining opportunities — modularity-preserving (the backend seam STAYS)
The `CgTarget` → `NativeTarget` polymorphism is the architecture's asset (one