kit

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

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:
Mdoc/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