kit

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

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:
Mdoc/plan/PERF-IDEAL.md | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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