kit

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

commit b50e883e3b179503f8c2340695356869e6b7e602
parent f1221981c527ac1c956d1f182a658f7153dd3255
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 11:35:07 -0700

doc(plan): O0 code-size reduction roadmap vs tcc (PERF-O0-CODESIZE.md)

Refreshed measurement: kit -O0 .text is 1.33x tcc on sqlite3.c (455,546 vs
342,735 insns), down from 1.57x after the value-stack-residency work — and the
#1 excess is now 'mov' (+70K), not 'sub'. Documents the opcode-histogram
diagnosis, the frame-addressing tax, and four ranked levers:

  1. Fix B positive-offset frame addressing (~-33K/-7.3% -> ~1.24x) — extends
     the existing bottom-record (fp_at_bottom) + AAPatch defer-patch infra to
     the -O0 single-pass path; alloca-safe via a stable anchor (tcc's x29 role).
  2. epilogue slim (-2.6K), 3. load-with-extend ldrsb/ldrsh (-1.3K),
  4. compute-into-place (mov/spill): lazy dest placement is the *absence* of
     eager relocation, not an added pass — compile-neutral-to-faster; phased
     plan (x0 result-adoption, dest-hint surface, args-into-arg-regs).

Added to doc/plan/README index.

Diffstat:
Adoc/plan/PERF-O0-CODESIZE.md | 400+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/plan/README.md | 1+
2 files changed, 401 insertions(+), 0 deletions(-)

diff --git a/doc/plan/PERF-O0-CODESIZE.md b/doc/plan/PERF-O0-CODESIZE.md @@ -0,0 +1,400 @@ +# Shrinking kit's -O0 emitted code toward tcc — the code-size track + +Companion to `PERF.md` (compile-speed standings), `PERF-TCC-GAP.md` (structural +diagnosis), and `PERF-TCC-SLIM.md` (the vtable-slimming compile-speed campaign). +Those chase **fewer instructions to compile**. This doc chases **fewer +instructions emitted** — making kit's `-O0` machine code as dense as tcc's. + +The two tracks compound. Every emitted instruction removed is also one fewer to +emit, relocate, and write to the object — so codesize wins shrink the +emit/objwrite/assemble phases *multiplicatively* (`PERF-TCC-SLIM.md` §2.1). This +track is most of the remaining gap to parity after the per-operation slimming. +The goal is **smaller and faster output while holding -O0 compile speed** — and +because un-emitted instructions cost nothing to emit, the well-chosen wins here +*improve* compile speed rather than trading it away. + +The arch focus is **aarch64** (the reference backend); x64/rv64 carry analogous +taxes and follow once the aa64 design is proven. + +--- + +## 1. Current state (measured 2026-06-13) + +`build/release/kit cc -c sqlite3.c` vs `tcc -c sqlite3.c`, arm64-macOS, on the +3.50.2 amalgamation (`tmp/projects/sqlite-amalg/sqlite3.c`, 9.28 MB). + +| metric | kit | tcc 0.9.28 | ratio | +|---|--:|--:|--:| +| **machine code (`__text`/`.text`)** | **1,822,184 B / 455,546 insns** | **1,370,940 B / 342,735 insns** | **1.33×** | +| object file | 2,216,680 B (Mach-O) | 2,108,134 B (ELF) | *not comparable¹* | +| compile instructions | 1,968 M | 667 M | 2.95× | +| compile wall | 0.18 s | 0.06 s | 3.0× | + +¹ tcc emits ELF, kit Mach-O — different format/symbol/string-table overhead. +**The honest size metric is the `.text` machine code**, and there kit is **1.33× +tcc, +112,811 excess instructions.** (This is well down from the 1.57× recorded +when §6's history was written — the value-stack-residency + lazy-homes + +coalescing commits landed since and reshaped the picture; `mov`, not `sub`, is +now the #1 excess.) + +**Reproduce:** + +```sh +KIT=build/release/kit; TCC=tmp/tinycc/tcc +SRC=tmp/projects/sqlite-amalg/sqlite3.c; SDK=$(xcrun --sdk macosx --show-sdk-path) +"$KIT" cc -c "$SRC" --sysroot "$SDK" -o /tmp/kit.o # text size: kit size /tmp/kit.o +"$TCC" -c "$SRC" -o /tmp/tcc.o # kit size /tmp/tcc.o +# opcode histogram diff: +"$KIT" objdump -d /tmp/kit.o | grep -E '^[[:space:]]+[0-9a-f]+:' \ + | sed -E 's/.*\t([a-z][a-z0-9._]*).*/\1/' | sort | uniq -c | sort -rn +``` + +--- + +## 2. Diagnosis — where the 112,811 excess instructions live + +Per-mnemonic signed diff (kit − tcc), from the opcode histograms: + +| mnemonic | kit | tcc | **excess** | what it is | +|---|--:|--:|--:|---| +| `mov` | 73,824 | 3,701 | **+70,123** | reg-reg copies — **#1 lever** | +| `stur` | 62,819 | 15,899 | **+46,920** | frame spills (negative-offset, unscaled) | +| `ldur` | 80,853 | 37,994 | **+42,859** | frame reloads | +| `sub` | 33,758 | 6,713 | **+27,045** | **29,379 are `sub xN,x29,#imm`** — far-local address building | +| `cbz` | 8,942 | 2,648 | +6,294 | compare-branch lowering | +| `movk` | 4,360 | 357 | +4,003 | building frame offsets > 4095 | +| `uxtb`/`uxth`/`sxtb`/`sxth` | 5,434 | 333 | +5,101 | sub-word extends after narrow loads | + +By value class the spill story is starker than the per-mnemonic split: **kit +stores ~78 K vs tcc ~26 K (3× the spilling)**; loads are closer (107 K vs 92 K). + +Where **kit already beats tcc** (do not touch — these are structural wins, mostly +kit's fused `cmp;b.cond` vs tcc's `cset;cbnz`, plus folded offsets): + +| mnemonic | excess | note | +|---|--:|---| +| `ldr` | −28,365 | tcc materializes more addresses; kit folds offsets into displacement | +| `add` | −18,422 | "" | +| `cset` | −17,226 | tcc lowers every compare to a materialized bool | +| `cbnz` | −10,817 | "" | +| `cmp` | −9,421 | kit fuses compare into the branch | +| `movn` | −6,720 | tcc's far-local offset build (see §3) | +| `nop` | −5,834 | kit's prologue pad is *smaller* than tcc's (§3) | +| `stp` | −3,722 | tcc pairs more aggressively | + +**Concentration.** The excess lives in the giant functions. Top 12 by size hold +~80 K insns; `_sqlite3VdbeExec` alone is **22,991** (5% of all text), of which +**5,162 (22%) are pure far-local address arithmetic** (2,757 `sub xN,x29` + +2,405 `movk`). Frame-size distribution: kit mean **112 B**, max 3,520, 181 +functions > 256 B; tcc mean **53 B**, max 3,392, 58 functions > 256 B. + +--- + +## 3. The two structural realities + +**(a) The frame-addressing tax** (`sub` +27 K, `movk` +4 K, and part of +`stur`/`ldur`). Both kit and tcc home fixed locals at **negative x29 offsets** — +this is *not* an addressing-mode difference. aarch64 reaches negative offsets +only with unscaled `stur`/`ldur` (±256). Past that, an address must be built. The +difference is *how*: + +- **kit** emits `sub x17, x29, #off` **per access** (+ a `movk` when off > 4095), + then `stur`/`ldur [x17]`. Cited at `aa_fp_off_slot = -off` (negative), the + single-pass TOP-RECORD layout (`src/arch/aa64/native.c:112`). +- **tcc** falls through `arm64_ldrx` (`arm64-gen.c:317`) to **register-indexed** + addressing: `movn/movz x30, #off ; ldr/str [x29, x30]`, reusing x30. One `movn` + covers any frame < 64 KB, and the index reg is shared across nearby accesses. + +In `VdbeExec`: kit 2,757 `sub` + 2,405 `movk`; tcc **66** `sub`, 1,668 indexed +accesses, 1,719 `movn`. kit pays ~2–3 insns per far access where tcc pays ~1, and +**kit makes more far accesses** because it touches the frame more (reality (b)). + +**(b) kit computes-into-a-register-then-moves/spills; tcc computes-into-place** +(`mov` +70 K, stores +52 K). 8,511 movs just capture a return value out of x0; +~19 K cluster around `bl` (arg setup `mov mov bl` / retval `bl mov`); the rest are +materialization chains (`ldur;mov`) and value-stack copies. The value-stack +residency work (§6) moved partway — it traded frame round-trips for register +moves (net fewer insns, but `mov` count *rose*). Closing the rest is +destination-driven (lazy) value placement — which is the *absence* of eager +relocation, **not** an added pass, so it is compile-time-neutral-to-faster (§8). +The cost is correctness/invasiveness, not throughput. Treated as a separate +track (§4, Lever 4; mechanism + phased plan in §8). + +**Non-levers (ruled out empirically):** +- *Prologue nop pad.* kit reserves a `b <body>; nop×3` patch region per function; + tcc reserves `ARM64_FUNC_STACK_SETUP_SLOTS` nops too, patched in `gfunc_epilog` + (`arm64-gen.c:1417`). tcc has **more** nops (13,757 vs kit 7,923). Not a + differentiator. +- *Branch lowering.* kit's fused `cmp;b.cond` already beats tcc's `cset;cbnz` + (the −17 K `cset`, −10 K `cbnz`, −9 K `cmp` deficits). Leave it. + +--- + +## 4. Ranked open levers + +| # | Lever | Est. saving | Risk | Compile-speed | §| +|---|---|--:|---|---|---| +| **1** | **Fix B — positive-offset -O0 frame addressing** | **~33 K (−7.3%) → ~1.24× tcc** | med (alloca layout, CFI, base-reg) | neutral/faster | §5 | +| 2 | Epilogue slim (drop the x16 dance) | ~2,633 (−0.6%) | low | neutral | §6 | +| 3 | Load-with-extend (`ldrsb/ldrsh` vs `ldrb;sxtb`) | ~1,300 | low-med | neutral | §7 | +| 4 | Compute-into-place (`mov`/spill; lazy dest placement) | up to ~50–70 K | high (correctness/invasiveness) | neutral/faster | §8 | + +Levers 1–3 are smaller-and-faster with bounded risk; Lever 4 is the deep, +high-count frontier — high *correctness* risk and invasiveness, but +compile-time-neutral-to-faster (it removes work, §8), so it is *not* gated by the +compile-speed budget. Re-profile after each landing — the phase shares shift. + +--- + +## 5. Lever 1 — positive-offset -O0 frame addressing (Fix B) + +**The idea:** address fixed locals as **positive *scaled* offsets** from a stable +frame-base anchor. A scaled `ldr/str [base, #+ofs]` reaches **32,760 B** for +64-bit (4095×8), covering any realistic frame in **one instruction, zero address +building** — strictly better than both kit's current `sub`-per-access *and* tcc's +`movn`+indexed scheme. This deletes essentially all 29,379 `sub xN,x29` and the +~4,000 frame-offset `movk`. + +### 5.1 The infrastructure already exists + +The aa64 backend already has a positive-offset layout and a deferred-patch list: + +- **BOTTOM-RECORD layout** (`fp_at_bottom`): the saved pair sits at the *bottom*, + `fp = sp`, slots stack **above** at positive offsets, `aa_fp_off_slot(a,off) = + frame_size − off` (`src/arch/aa64/native.c:291`, layout diagram :120-136). It is + currently **gated to -O1 known-frame** small frames with `out_stack == 0` + (`native.c:1835`), because the positive offset `N − off` needs `frame_size` (N), + which the single-pass path does not know before the body (`native.c:217`). +- **`AAPatch` deferred-patch list** (`native.c:185-202`, resolved in + `aa_func_end`): the single-pass path **already defers the prologue `sub sp`** and + patches it once `frame_size` is final (`native.c:72-82`). The same mechanism + extends to slot offsets. + +So Fix B is **not a new subsystem** — it is *extending BOTTOM-RECORD to the -O0 +single-pass path* by deferring the per-slot offset, reusing `AAPatch` and the +deferred-prologue machinery. + +### 5.2 The single-pass mechanic + +1. Emit each slot access as a scaled `str/ldr [base, #placeholder]` (imm12) and + record an `AAPatch{insn_pos, slot_off, scale}`. +2. In `aa_func_end`, once `frame_size` (N) is final, patch `imm12 = (N − slot_off) + >> scale`. Because positive scaled imm12 reaches 32,760 B, the placeholder is + **always one word** — no 1-word→2-word expansion (the old worry, which was a + *negative*-offset artifact, is gone). +3. The base anchor is set once in the prologue and never clobbered. Two viable + anchors (decide during implementation): + - **`fp` at the bottom** (true BOTTOM-RECORD): `mov x29, sp` *after* the + deferred frame `sub`. fp becomes the stable bottom anchor. CFA becomes + `fp + N` (frame-size-dependent — patch the CFI in `aa_func_end`, see §5.4). + - **A dedicated callee-saved base reg `xFB`**: keep fp at the top (top-record + CFA `fp+16`, unwind unchanged) and add `mov xFB, sp` after the frame `sub`. + Costs one reserved callee-saved register + its save/restore, but leaves + unwind/CFI untouched. Note the single-pass path currently *uses no + callee-saved registers* (`native.c:77`, enforced in `nd_scratch_acquire`) — + reserving `xFB` relaxes that invariant. + +The setup insn lands in the existing reserved prologue region, so **only +functions that overflow ±256 pay it** — small functions keep today's +`ldur [x29,#-n]` and emit nothing extra. + +### 5.3 alloca / VLA — why this is *not* a fallback problem + +How **tcc** handles alloca (`arm64-gen.c:2305-2334`): `gen_vla_alloc` does +`sub sp, sp, xN` at runtime; `gen_vla_sp_save/restore` spill/reload sp to a frame +slot at scope boundaries. **Fixed locals stay x29-relative; sp floats.** x29 is a +stable anchor that alloca cannot disturb. + +Fix B's base anchor (fp-at-bottom or `xFB`) **plays exactly tcc's x29 role** — it +is set once and is unaffected when alloca lowers sp. The VLA memory is sp-relative +(below the anchor); fixed-local addressing through the anchor is untouched. So +alloca is **safe by construction, with no `CG_FUNC_HAS_ALLOCA` flag and no +fallback path** — that worry was specific to a *bare-sp-relative* scheme. + +Caveat to settle: today's BOTTOM-RECORD excludes `has_alloca` *and* requires +`out_stack == 0` (`native.c:1835`). The -O0 variant must support `out_stack > 0` +(outgoing stack args coexist with positive slot addressing) and alloca. Cleanest +first cut: choose the layout **at `aa_func_end`**, where `has_alloca` is finally +known — patch slots positive (Fix B) when no alloca was seen, or fall back to the +current negative top-record `sub` form when it was. The deferred-patch list makes +this a per-function decision at resolve time, not an up-front guess. + +### 5.4 Open design points + +- **CFI.** fp-at-bottom makes CFA frame-size-dependent (`fp + N`); emit/patch the + CFA rule in `aa_func_end`. The `xFB` variant avoids this entirely (fp stays at + top). Gate on `test-dwarf`/`test-debug`. +- **Sub-word reach.** Scaled imm12 reaches only 4095×size — 16 KB for 4-byte, + 8 KB for 2-byte, 4 KB for 1-byte. Frames past those for *narrow* accesses keep + the indexed/`sub` fallback. Rare; the 64-bit/32-bit common case covers nearly + everything. +- **Index registers (`[base, xN, lsl #s]`) and q-regs.** Verify the scaled forms + for 128-bit (reaches 65,520 B) and the few indexed loads. +- **`frame_size_final` plumbing.** It is "set in `aa_func_begin_known_frame` … + unread on the single-pass path" (`native.c:214-217`). The single-pass path must + now compute and store it before the patch loop in `aa_func_end`. + +### 5.5 Expected payoff & gate + +**−29,379 `sub` − ~4,000 frame `movk` ≈ −33 K insns (−7.3% text) → ~1.24× tcc**, +concentrated in the giant functions (`VdbeExec` alone −5,162, ~22% of the +function). Setup cost is ~1–3 insns only in big-frame functions, vastly +out-weighed. Compile speed is **neutral-to-faster** (bounded O(far-accesses) patch +list; ~33 K fewer instructions to emit). + +**Gate: run-correctness + determinism** (it changes emitted bytes): compile +sqlite twice and `cmp` the objects (self-identical), `make test-toy test-parse-ok +test-parse-err test-smoke-x64 test-smoke-rv64 test-dwarf test-debug`, the sqlite +e2e (`84|2`), and a clang-differential probe over alloca/VLA + large-frame + +&local-address-taken + outgoing-stack-arg functions. + +--- + +## 6. Lever 2 — epilogue slim (quick win) + +kit emits a **3-insn** epilogue using x16 as scratch: + +``` +add x16, x29, #0 ; copy fp to scratch +ldp x29, x30, [x16] ; restore fp/lr +add sp, x16, #16 ; sp = fp + 16 +ret +``` + +tcc emits **2 insns** (`arm64-gen.c:1668`): + +``` +mov sp, x29 ; sp = fp (recovers from any alloca too) +ldp x29, x30, [sp], #16 ; restore fp/lr, post-increment sp +ret +``` + +`mov sp, x29` then a post-indexed `ldp [sp],#16` needs no scratch and is correct +under alloca (it restores sp from fp). **−2,633 insns (−0.6%).** Byte-safe modulo +the intended change; investigate why the x16 form was chosen (likely incidental). +Gate: run-correctness + determinism + the alloca probe above. + +--- + +## 7. Lever 3 — load-with-extend (quick win) + +kit emits **0** `ldrsb`/`ldrsh`; tcc emits 1,313. kit lowers a signed narrow load +as `ldrb; sxtb` (two insns) where one `ldrsb` suffices. This was flagged blocked +in `[[o0-copy-convert-coalescing]]` (needs a load-with-extend rider on the +`MemAccess`, not a register rename). **~−1,300 insns.** Gate: run-correctness + +determinism; signed/unsigned char/short load corpus. + +--- + +## 8. Lever 4 — compute-into-place (the `mov`/spill frontier) + +The biggest *raw* counts — `mov` +70 K and stores +52 K — are reality (b): kit +computes into a register, then moves/spills to place. This is the largest lever, +and the one most worth understanding correctly, because the obvious framing +("add a register allocator / a cleanup pass") is wrong and would indeed cost +compile time. **The correct framing is the opposite: compute-into-place is the +*absence* of eager relocation, not extra analysis — it emits fewer instructions +and runs no extra pass. tcc is faster *because* of it.** So this lever is +compile-time-neutral-to-*positive*; its real cost is correctness/invasiveness, +not throughput. + +### 8.1 How tcc does it — lazy location + demand-driven registers + +tcc's `SValue` separates *what a value is* from *where it lives* (`tcc.h:479`). A +value-stack entry stays unmaterialized — a constant, a memory ref +(`VT_LOCAL|VT_LVAL`), CPU flags (`VT_CMP`), or a register — until something +*forces* it. When forced, `gv(rc)` materializes it into a register **of the +class the consumer asks for** (`tccgen.c:1844`); `get_reg(rc)` picks that +destination *at the point of use* (`tccgen.c:1487`); the result lands there and +`vtop->r` is updated to point at it. There is no IR, CFG, or second pass — the +"analysis" is the recursion shape: the parent passes down the register it wants. +Laziness is therefore *cheaper*, not costlier. + +### 8.2 The three mov classes — tcc vs kit (with emit sites) + +| class | excess | tcc | kit today | +|---|--:|---|---| +| **retval capture** | ~8,511 | result SValue records `r = x0`, emits nothing; next forcing use materializes it (`tccgen.c:6334`) | pre-allocates a cache reg, emits `mov cachereg,x0` after `bl` (`native_direct_target.c:1952,2010`) | +| **arg setup** | ~19,000 | `gv(RC_R(n))` materializes each arg **directly into x0–x7** at the call (`arm64-gen.c:1187`) | args already sit in general cache regs; `native_arg_shuffle` moves them into x0–x7 | +| **materialize chains** (`ldur;mov`) | ~18,000 | `gv` loads straight into the consumer's demanded reg | `nd_materialize_operand` loads into an *arbitrary* scratch, then `mov`s to the consumer's reg (`native_direct_target.c:832,954`) | + +### 8.3 Why kit pays — three missing handoffs, not missing analysis + +kit's NDT cache is *already* a lazy-location model, and its bookkeeping is +already cheap (`nd_cache_alloc` O(≤8), `nd_touch` O(1), flush O(ncached)≤8). The +movs come from three gaps: + +1. **ABI registers are excluded from the cache.** x0–x7 are non-allocable + (`aa64/native.c:3843`), so a result can't *stay* in x0 and an arg can't be + *produced into* x_k — both route through a general cache reg + a mov. +2. **No destination-hint surface.** A producer always writes to a + cache-allocator-chosen register; no consumer can say "produce this in + register R." The only operand hint today is `OPK_FLAG_KILL` (`cgir.h:210`), + which transfers ownership of a *dead source* but does not request a + *destination*. +3. **kit materializes args eagerly, before the call.** By the time `nd_call` + runs, args are CGLocals already in cache regs → they must be shuffled. tcc + materializes args *at* the call, while still unmaterialized, straight into + arg regs. + +### 8.4 Phased plan — cheapest-first, each compile-neutral-or-faster + +Each phase adds a *preferred-register field*, not a pass; every mov removed is +also one fewer instruction to emit (faster compile). Uniform gate: +**run-correctness + determinism** (compile twice + `cmp`; full +toy/parse/smoke/dwarf/debug; sqlite e2e `84|2`; clang-differential probes around +calls/varargs/sret). The one real correctness surface is the **eviction +discipline for ABI-register holders**. + +- **Phase 1 — adopt x0 as the result holder (~8,511 movs, the clean win).** + Record "this result local *is in* x0" as a short-lived cache entry instead of + allocating a cache reg + emitting `mov cachereg,x0`. The next call already + evicts caller-saved cache regs — reuse that path to spill/move x0 only if it + is still live at the next clobber. For `x = f()`, `f()+1`, `g(f())` (result + consumed before the next call) it is **zero movs**. Self-contained, reuses the + flush-at-call machinery, exactly tcc's "retval stays in x0." Correctness rule: + evict the x0-holder before *any* op that clobbers x0 (calls, and x0-using + lowering like div/varargs). +- **Phase 2 — a destination-hint field on the operand (~18 K materialize + chains).** Add an optional "preferred destination register" beside + `OPK_FLAG_KILL`; when the consumer's wanted reg is known and free, + `nd_materialize_operand` loads straight into it. The cache already supports + "write into reg R" — only the hint needs to flow. This is the general form of + tcc's `gv(rc)`. +- **Phase 3 — args into arg-regs (~19 K, the structural one).** Thread the hint + from the call *back* to the arg producers (the frontend knows, when generating + arg-expr k, that its result feeds arg slot k — pass an abstract "destined for + arg k" the arch resolves to x_k), or keep args unmaterialized until `nd_call`. + Non-conflicting args (the majority) then produce directly into x_k; + `native_arg_shuffle` still resolves the rare cross-arg cycles (tcc handles the + same case by spilling conflicts to stack first). Biggest payoff, most + plumbing — do last. + +Prior partial work this builds on: value-stack residency (register-resident +return/args/result), lazy transient homes, lazy-dup + copy/convert coalescing +(`[[o0-value-stack-residency]]`, `[[o0-copy-convert-coalescing]]`). + +History of landed codesize work (context for §1's 1.33×), newest last: + +- temp-slot reuse `3516b3d3`, addr-result caching `f75874cb`, subscript fusion + `442fdc55`, transient-liveness + lazy-dup `ecbd00dc`/`73e128a2` — took the + object 3.45×→1.57× tcc (`[[o0-codesize-vs-tcc]]`). +- value-stack residency (register-resident return/args/result + spill-only-live- + set), lazy transient frame homes, copy+convert coalescing — took emitted insns + ~539 K→455 K and the ratio 1.57×→1.33× (`PERF-TCC-SLIM.md` §10c/§10d, + `[[o0-value-stack-residency]]`, `[[o0-lazy-transient-homes]]`). + +--- + +## 9. Gate & measurement methodology + +- **Measure on RELEASE** (`make bin RELEASE=1`); the ASan default inverts costs. +- **Size metric = `.text` machine code**, not object bytes (format-skewed). Use + `kit size` / the §1 histogram diff. The instruction metric is deterministic to + ±0.2 M on a fixed binary — small deltas are real. +- **Codesize changes alter emitted bytes**, so the gate is **run-correctness + + determinism** (compile twice + `cmp`; full toy/parse/smoke/dwarf/debug; sqlite + e2e `84|2`; clang-differential probes), *not* the byte-identity gate used for + compile-speed-only refactors. +- Re-profile after each landing and re-rank §4 — the highest lever moves as work + is removed. diff --git a/doc/plan/README.md b/doc/plan/README.md @@ -11,6 +11,7 @@ shrinks to whatever remains open. | [RELEASE.md](RELEASE.md) | Cross-cutting initial-release punchlist: release scope, deferred features, and per-subsystem completion/validation items. | — | | [OPTIMIZER.md](OPTIMIZER.md) | Completing the O2 SSA mid-end, expanded inlining, -O0/-O1 performance work, machine register-constraint improvements. | [../OPT.md](../OPT.md) | | [PERF.md](PERF.md) | Making kit the fastest `-O0` C compiler: the compile+link scaling benchmark (`make bench-cc`), per-axis linearity goals, hotspot sampling, and the open superlinear-axis fixes. | — | +| [PERF-O0-CODESIZE.md](PERF-O0-CODESIZE.md) | Shrinking kit's `-O0` emitted machine code toward tcc (1.33× → parity): the opcode-histogram diagnosis, the frame-addressing tax, and the ranked levers — positive-offset frame addressing (Fix B), epilogue slim, load-with-extend, and the deferred `mov`/spill track. Companion to PERF-TCC-{GAP,SLIM}.md. | [../ARCH.md](../ARCH.md) | | [LINKER.md](LINKER.md) | Incremental linking: the file-based object-link redesign and remaining non-ELF format coverage. | [../LINK.md](../LINK.md) | | [JIT.md](JIT.md) | Function-level hot reload, Go-runtime-style codegen support, and remaining JIT host-portability work. | [../JIT.md](../JIT.md) | | [DEBUG.md](DEBUG.md) | The Windows debugger host adapter, x64/rv64 displaced single-step, profiling, and DWARF gaps. | [../DBG.md](../DBG.md), [../DWARF.md](../DWARF.md) |