kit

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

commit f1221981c527ac1c956d1f182a658f7153dd3255
parent 8a6cde7b8cd3e0c8c7ed704b78f89df57a99b45c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 10:51:43 -0700

doc(perf): catalog seam-preserving opportunities — front-half bricks, type, NDT-internal, codesize track (modularity kept)

Diffstat:
Mdoc/plan/PERF-IDEAL.md | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 100 insertions(+), 0 deletions(-)

diff --git a/doc/plan/PERF-IDEAL.md b/doc/plan/PERF-IDEAL.md @@ -479,6 +479,106 @@ 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. +## Remaining opportunities — modularity-preserving (the backend seam STAYS) + +The `CgTarget` → `NativeTarget` polymorphism is the architecture's asset (one +frontend, seven backends: aa64/rv64/rv32/x64 native + wasm + c_target + check, plus +the opt-IR recorder at -O1) and is **out of scope** — we are not collapsing the +value-stack→direct-emit seam, not monomorphizing it, not adding a parallel +-O0 fast path that bypasses the public `kit_cg_*` API. Everything below respects +that line. It lives in one of four places that are **not** the swappable seam: +(a) the front half (lex/pp/intern/parse), (b) *inside* the concrete +`NativeDirectTarget` and the monomorphic `MCEmitter` (the -O0 register machinery +and byte sink — these are not the polymorphic vtable), (c) the type subsystem, or +(d) the codesize track (which *bytes* the NDT chooses to emit, not how it dispatches). + +> **Reframe that governs this list (verified):** kit is *already* AST-free and +> IR-free at -O0 (`session.c:148` gates the opt IR behind `opt_level > 0`; the +> parser emits as it parses; the `p->replay[]` buffer is bounded local lookahead +> for incomplete-array brace-init sizing only, `parse_type.c:1746`). tcc's "no +> second copy of the program" property is already true here. So none of the items +> below is "delete a second representation" — they are **thin-the-unit / +> classify-once / emit-fewer-bytes**, the same conservation laws as the rest of +> this doc. + +The re-profile shows no remaining single function of the WAVE-1 class. The biggest +remaining *lever* is the codesize track (§R-D, multiplicative); the highest-value +single *brick* is the symbol-binding cache (§R-A1). + +### §R-A — Front half: lex / pp / intern / parse (~50% of self-Ir) + +| # | opportunity | where | character | gate | +|---|---|---|---|---| +| **A1** | **Symbol-binding cache on the interned `Sym`** (the front-half "single-pass brick"; SLIM §B5). `scope_lookup` is an N-scope chain walk per identifier (`parse.c:378`, 12 call sites); tcc caches `sym_identifier` on the interned token. A `Sym`-keyed binding stack with push/restore on scope enter/exit makes resolution a pointer load. | `parse.c:301-380` (scope), interner | **structural, high value** (depth-dependent, ~5-10% of parse phase) | byte-identical; **medium-high risk** — save/restore + shadow/redef/typedef-vs-ident discipline; full parse corpus | +| A2 | **Re-diagnose `lex_open_mem`'s 79M** (WAVE-3 proved it is NOT the memset). It is genuine per-open work over ~6,000 opens (3,140 files + 2,796 macro-paste buffers): field init, the splice-fold loop control, `kit_source_add_memory`, `lex_catchup_splices`. Profile *what*, then cut per-open setup — esp. whether macro-paste buffers (`<paste>`) need a full `lex_open_mem` at all. | `lex.c:281-305`, `pp_expand.c` paste sites | unknown until re-profiled; potentially structural | byte-identical | +| A3 | **Macro body replay by pointer-swap** (gap #3 / SLIM §A5). `subst_phase2` copies the object-macro body even with no `##`; function-macro args take 3-4 copies/token. A `has_paste` flag set at definition → replay the immutable body directly (tcc's model). | `pp_expand.c` (`subst_phase2`, arg subst) | structural, PP-contained | byte-identical (macro corpus) | +| A4 | **Stop materializing non-directive newlines** (deeper §C.3b). Today they are drained in place (`bfa30f77`); the full version never builds them as `Tok`s on the cc path (lexer sets `TF_AT_BOL`; `-E` keeps a newline-emitting variant). ~51% of lexer outputs are newlines. | `lex.c:644`, `pp.c`/`pp_directive.c` | structural, **med-high risk** (cc-vs-`-E` split; directive-line contract) | byte-identical, gate the `-E` categories hard | +| A5 | **PP per-token micro-levers**: gate the `kit_arena_reset` reset-check on "xarena actually grew" (fires ~every 5 tokens); tighten the `#line`-delta + include-guard state machine on the `src_next_raw_into`/`pp_pull_into` hot path. | `pp.c:949`, `pp_expand.c:935` | cleanup (~1-3M) | byte-identical | +| A6 | **`pool_intern_slice` probe/insert side** (71.6M; the WAVE-1 cache took the hash side). Diminishing — the self-sufficient `{hash,sym}` slot is measured **~0 Ir** (a D1/wall-clock play, not instructions). Only revisit under a cache-stall study. | `pool.c` | **near-dead-end for Ir** | — | +| — | `lex_next` (16.5%, the dominant fn) and the `Tok` relay (gap #1) are **already optimal** — raw-cursor scanner; `_into` out-pointer relay; `Tok`-16B was a measured dead end. No structural lever left. | — | done | — | + +### §R-B — Type subsystem (~14% of self-Ir) + +| # | opportunity | where | character | gate | +|---|---|---|---|---| +| B1 | **Kill the per-call `id → entry` decode** that is now the residual hub cost (`api_type_pred` 61.9M, `cg_type_get` 26.6M, `api_unalias_type` 53.7M). §D.2 made the *result* O(1); §D.3b carried the `CgId` on the parser slot — go one further and carry the **decoded `CgApiType*` entry** (or inline `api_type_from_id`'s segvec double-index) so a predicate is a direct field read, not a decode. | `src/cg/type.c` (`api_type_from_id`, `api_type_pred`), `cg_adapter` | structural-ish (the next type lever) | byte-identical | +| B2 | **Collapse `c_abi_type_info` into a descriptor reader** (§D.4, deferred). Two memos compute the same facts on two keys (`Type*` vs `CgId`). Blocked by a layer boundary (frontend has no `KitCompiler*→TargetABI*` accessor) + the `signed_` divergence + a `scalar_kind` ALIAS edge — needs a small public accessor first. | `lang/c/abi/c_abi.c:34`, `src/abi/abi.c` | cleanup (~8M), boundary work | byte-identical | +| B3 | **`resolve_type` 26.7M / `type_cg_lower` 16M / `type_cg_id_in_pool` 13.4M** — the `Type*`→`CgId` bridge. §D.3b cut the *operand* re-crossings; `resolve_type` (frontend type resolution) is separate and still hot. Audit for a re-resolve that a per-`Type` cache would remove. | `lang/c/type/type.c`, `cg_adapter` | cleanup | byte-identical | + +### §R-C — Inside the concrete NDT + the monomorphic MCEmitter (NOT the vtable, ~7%) + +These are the -O0 register machinery and byte sink *within* `NativeDirectTarget` / +`MCEmitter` — concrete, not the swappable dispatch. Fair game. + +| # | opportunity | where | character | gate | +|---|---|---|---|---| +| C1 | **Free-register bitmap** for `nd_cache_alloc`'s free-pool scan (the policy-neutral half — find-first-set on `caller_saved & ~owned` instead of the linear scan). The LRU victim policy (D2 round-robin) was **dropped** — it is inlined/invisible to Ir and risks the codesize track. | `native_direct_target.c` (`nd_cache_alloc` 0.75%, `nd_dst_reg`, `nd_dst_writeback`) | cleanup (~1-2M) | byte-identical | +| C2 | **Typed-store for memory-op emit** — §E.1's `mc_emit32` covered the fixed-width path; `aa_emit_mem` (0.81%) and the other multi-byte idioms still stage+`mc_emit_bytes`. Extend the cursor store to them. | `aa64/native.c` (`aa_emit_mem`), `mc.c` | cleanup (~few M) | byte-identical | +| C3 | **Contiguous section image** (gap #5 / D5-stretch) — a single geometric section buffer so `mc_emit32` stores into the *final* image (the full tcc shape for emit), eliminating `buf_flatten` at objwrite. MCEmitter is monomorphic, so this never touches the seam. | `mc.c` (cursor), `obj/{elf,macho}/emit.c` | structural (the emit brick); Ir win small (~0.25M flatten) but it is the last "emit into final image" piece | byte-identical (`test-elf/macho/link`) | +| C4 | **objwrite mop-up** (D7): single-pass symtab emission, chunk-walk the strtab. | `obj/macho/emit.c`, `obj/elf/emit.c` | cleanup | byte-identical | +| — | **`ApiSValue` 56→24 B shrink (C3)** — the value node is CG-layer infra (not the vtable, so technically in scope) but the shrink was **measured neutral** (Round 7) and `api_sv_adjust_refs` (0.77%) is the load-bearing liveness refcount that drives coalescing, not copy bandwidth. Leave it. | `src/cg/internal.h`, `value.c` | measured-neutral | — | + +### §R-D — The codesize track (the BIGGEST remaining lever — multiplicative, seam-independent) + +kit's -O0 `__TEXT` for sqlite is **1.88 MB, ~2.4× tcc's** (`o0-codesize-vs-tcc`). +This is the *what-bytes-NDT-emits* axis — it does **not** touch the dispatch seam, +and it is **multiplicative**: every byte removed cuts emit + objwrite + assemble +together. The value-stack residency + coalescing + lazy-homes work already took the +object 4.26 MB → 2.22 MB (~1.0× tcc object); the remaining ~2.4× *text* bloat is +the single largest lever left after the front half. Open items (from +`PERF-TCC-SLIM` Track B / the `o0-codesize` track): + +- **Signed-load fold** `ldrb;sxtb` → `ldrsb` (and the zero-extend cases) — + structurally **blocked**: needs a load-with-extend rider on `MemAccess`, not a + register rename (the copy/convert-coalescing work flipped the register cases but + could not reach the load itself). +- **Call-argument spill** reduction (fewer stack stores around calls). +- **Indexed-addressing fold** (collapse base+index into one addressing mode). +- **NOP / alignment pad** trimming. +- Each is **run-correctness + determinism**-gated (it deliberately changes emitted + bytes), not byte-identical — and each compounds with the per-op emit costs in §R-C. + +This is the track to open next: it is the largest remaining lever, it is entirely +within the NDT's byte choices (seam untouched), and it directly shrinks the worst- +ratio phase. + +### §R-E — Memset / arena churn (~2-3%) + +| # | opportunity | where | character | +|---|---|---|---| +| E1 | Right-size the per-expression / per-emit struct zeroing (`__GI_memset` ~26.5M); designated-init the per-op clears (the idiom `api_op_imm`/the B2 `PcgLvAux` work already use). | `cg_adapter.c`, `value.c`, `native_direct_target.c` | cleanup | +| E2 | Arena churn (`arena_alloc` 20.4M, `arena_reset` 14.2M) — audit per-statement/per-temp allocation vs reuse. | `src/core/arena.c` consumers | cleanup | + +### Explicitly OUT of scope (the seam stays modular) + +The gap-#4 value-stack→direct-emit collapse — removing/monomorphizing the +`CgTarget`/`NativeTarget` vtable indirection, or adding a parallel -O0 native path +that bypasses the public `kit_cg_*` API. The two real indirect calls per primitive +(the `CgTarget` seam + the per-arch `NativeTarget` dispatch) are the price of the +seven-backend modularity and are **kept**. The ~2× instruction headroom that a full +tcc-shape collapse might reach is **not** pursued; the seam-preserving items above +plus the codesize track are the campaign from here. + ## Appendix — claims verified against source (not docs) - §A scalar splice loop + no NEON: `lex.c:156-157`, disasm of the profiled binary. ✓