commit 508a6344ac3365fcf0fe738853fa8ab690f54776
parent 811351c2b5e379813b0838812cab66d2425663ec
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 22:25:07 -0700
doc(perf): mark §4.1 #2a/#2c, #3, #4 Cut A/B landed (−2.29% -c on sqlite)
Diffstat:
| M | doc/plan/PERF.md | | | 67 | ++++++++++++++++++++++++++++++++++++++++++++----------------------- |
1 file changed, 44 insertions(+), 23 deletions(-)
diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md
@@ -30,7 +30,7 @@ release kit at `build/release/kit` (`make bin RELEASE=1`).
## 1. Current standings
-### Compile speed — the open frontier (~2.9× tcc)
+### Compile speed — the open frontier (~2.8× tcc)
Apple-silicon arm64 / macOS. **`instructions` is the metric to trust** —
load-independent (`/usr/bin/time -l`, best-of-7); cycles/wall are low-load
@@ -39,12 +39,12 @@ readings (`sysctl -n vm.loadavg`) shown only for context.
| compiler | instructions | cycles † | wall † | object |
|---|--:|--:|--:|--:|
| **tcc 0.9.28** | 0.66 B | 0.19 B | 0.06 s | 2.11 MB |
-| **kit** | 1.91 B | 0.52 B | 0.16 s | 1.83 MB |
+| **kit** | 1.87 B | 0.52 B | 0.16 s | 1.83 MB |
| clang 22 | 8.73 B | 2.57 B | 0.81 s | 1.50 MB |
† low-load; instructions is the figure to trust.
-kit beats clang and is the fastest *general* backend, but **tcc is ~2.9×
+kit beats clang and is the fastest *general* backend, but **tcc is ~2.8×
(instructions) ahead** — this is the whole game. **The gap is instructions, not
cache:** kit's IPC (~3.7) is *higher* than tcc's (~3.5), so both are compute-bound
on a wide core and there is no hidden cache-miss penalty to claw back. The payoff
@@ -243,31 +243,52 @@ not the swappable vtable), or the code-size track (which *bytes* the NDT emits).
### 4.1 Compile speed (frontend-bound)
-1. **Symbol-binding cache on the interned `Sym`** — the highest-value front-half
+**Status (this campaign).** Most of §4.1 has now landed, byte-identical, for a
+combined **−43.7 M `-c` instructions (−2.29 %)** on sqlite vs the pre-campaign
+snapshot (sqlite `.o` + `-E` bit-for-bit identical; 60/60 gate; full parse/pp/cg
+/toy/smoke-x64/rv64/debug/dwarf suites green). Item-by-item below: #1 ✅ (landed
+earlier, `5ff33ea3`); #2 ◐ (a+c landed `d07e43a3`+`c8ee9133`; b deferred —
+near-zero payoff, see note); #3 ✅ (object-macro replay `7ada3f62`, function-macro
+replay `d59dff08`, lexer-level newline elision `811351c2`); #4 ◐ (Cut A
+`f368c7f8` + Cut B `94e642e3` landed; the dead paste-file-id register, Cut C, left
+as a small follow-on); #5 ○ open.
+
+1. ✅ **[DONE — `5ff33ea3`]** **Symbol-binding cache on the interned `Sym`** — the highest-value front-half
brick. `scope_lookup` is an N-scope chain walk per identifier (`lang/c/parse/
parse.c`, ~12 call sites); tcc caches the binding on the interned token. A
`Sym`-keyed binding stack with push/restore on scope enter/exit makes resolution
a pointer load. Medium-high risk (save/restore + shadow/redef/typedef-vs-ident
discipline); byte-identical gate, full parse corpus.
-2. **Type subsystem (#2 self-`Ir` cluster, ~14 %).** Carry the *decoded* type
- entry on the parser/operand slot so a predicate is a field read, not an
- `id→entry` decode (`api_type_pred`, `cg_type_get`, `api_unalias_type` are the
- residual hub). Collapse the duplicate `c_abi_type_info` (frontend) vs
- `abi_cg_type_info` (backend) memos into one descriptor reader (needs a small
- public `KitCompiler*→TargetABI*` accessor first). Audit `resolve_type` /
- `type_cg_lower` for a re-resolve a per-`Type` cache would remove.
-3. **Token relay (lex+pp).** Replay macro bodies by **pointer-swap** when there's
- no `##` (today `subst_phase2` copies the body; function-macro args take 3–4
- copies/token) — set a `has_paste` flag at definition, replay the immutable body.
- **Stop materializing non-directive newlines** as `Tok`s on the cc path (~51 % of
- lexer outputs are newlines, drained downstream today; keep an `-E` variant that
- emits them). Both byte-identical-gated; the newline one is medium-high risk
- (cc-vs-`-E` split + directive-line contract).
-4. **`lex_open_mem` per-open work** (~6,000 opens: 3,140 files + 2,796 macro-paste
- buffers). Re-profile *what* (field init, splice-fold control, `lex_catchup_
- splices`) and cut per-open setup — especially whether `<paste>` buffers need a
- full `lex_open_mem` at all.
-5. **`memset` / arena churn (~2–3 %).** Right-size per-expression / per-emit struct
+2. ◐ **[a+c DONE; b deferred]** **Type subsystem (#2 self-`Ir` cluster, ~14 %).**
+ *Done:* single-decode the per-op predicate gauntlet — hoist one
+ `api_type_pred_bits` per id and test `API_PRED_*` masks locally instead of
+ re-calling `cg_type_is_*` (each a fresh `id→entry`+unalias decode) on the
+ load/store/convert hot paths (`d07e43a3`); fuse the redundant `resolve_type`
+ into the following `api_unalias_type`, and route the hot `cg_adapter` helpers
+ through the slot's cached `cg_id` instead of the uncached `pcg_tid` bridge
+ (`c8ee9133`). *Deferred:* collapsing the duplicate `c_abi_record_layout`
+ (frontend) vs `abi_cg_record_layout` (backend) memos — both are already
+ memoized at O(#distinct records), so the collapse saves only the duplicate
+ build (< 0.01 % of total) against real boundary/byte-identity risk; not worth
+ it now. Optional remaining: cache `pred_bits` on the `ApiSValue` slot itself
+ (free `ApiBitField.pad` bytes) — measure first, retype-invalidation surface.
+3. ✅ **[DONE]** **Token relay (lex+pp).** Macro bodies with no `##` now replay by
+ pointer (object-like `7ada3f62`; function-like skips the `subst_phase2` copy,
+ `d59dff08`). Non-directive newlines are no longer *materialized* by the lexer on
+ the cc path (`811351c2`, −32 M `-c`): an `emit_newlines` lexer mode suppresses
+ them while still emitting the one newline that terminates a directive line
+ (Option B2 — no cross-frame coupling); `-E`/cpp keep them. Line numbers safe by
+ construction (counter advances on byte-consume, `loc` rides every token);
+ verified bit-identical on `-E`, `-c`, and `-c -g` DWARF.
+4. ◐ **[Cut A+B DONE; Cut C open]** **`lex_open_mem` per-open work** (~6,000 opens:
+ 3,140 files + 2,796 macro-paste buffers). *Done:* paste buffers skip the
+ `lex_fold_splices` memchr scan via a guarded splice-free fast path (`f368c7f8`,
+ they are interned already-folded spellings — provably splice-free); the
+ per-file-open full-struct `memset` is right-sized to just the `punct_sym`
+ sentinel array (`94e642e3`). *Open (Cut C):* the dead paste file-id
+ registration could become a bare `nfiles++` after auditing no diagnostic ever
+ queries a paste file-id.
+5. ○ **[OPEN]** **`memset` / arena churn (~2–3 %).** Right-size per-expression / per-emit struct
zeroing (designated-init the per-op clears); audit per-statement/per-temp arena
allocation vs reuse. (`memset` here is explicit zero-init, **not**
`-ftrivial-auto-var-init` — proven by rebuilding with the flag off; attack call