kit

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

commit 1dcf1752ceb58c302140831ee751206d26e9959d
parent f61d252b3345168dc0d9706cb48ce485f10502cb
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 07:19:44 -0700

doc(perf): record -O0 value-stack residency results (PERF.md standings + SLIM §10c)

Diffstat:
Mdoc/plan/PERF-TCC-SLIM.md | 44++++++++++++++++++++++++++++++++++++++++++++
Mdoc/plan/PERF.md | 10++++++----
2 files changed, 50 insertions(+), 4 deletions(-)

diff --git a/doc/plan/PERF-TCC-SLIM.md b/doc/plan/PERF-TCC-SLIM.md @@ -378,6 +378,50 @@ it needs the **codesize track** (fewer emitted bytes → less emit/objwrite/ assemble, the multiplicative lever) and the value-stack/coalescing redesign. Treat the micro-levers as cleanups that compound, not as the path to parity. +## 10c. Measured results — -O0 value-stack residency (the high-value lever) + +§10b flagged the real lever as *"the value-stack/coalescing redesign,"* not the +per-op micro-slimming. That redesign landed as four staged, run-correctness + +determinism-gated commits on the brief *"stop homing intermediates in frame +slots: keep them register-resident on the value stack and spill only the live +set at calls."* The diagnosis (from emitted `-O0` asm) was three round-trip +classes, all instances of *frame-home every intermediate + flush-everything at +every barrier*: + +| commit | change | sqlite3.c -c -O0 effect | +|---|---|--:| +| `93aafda3` | **register-resident return** — `nd_ret` sources the value from its live register and drops the cache without spilling (every cached local is dead at a return); was `stur w8,[home]; ldur x0,[home]` | emitted 526,681 → 520,854; obj −0.9% | +| `2b1b03c2` | **assignment coalescing** — `pcg_store_void` (no dup of a discarded assignment result) + `kit_cg_store` materializes a delayed arith/cmp straight into the destination local; kills the temp→local routing `mov` for `x = <expr>` | compile −5.9M; emitted → 518,757 | +| `98b1f010` | **register-resident call args + spill-only-live-set** — a provably-dead arg (`CGCallDesc.arg_dead_mask` from `api_temp_dead`) flows reg→arg-reg via the existing `native_arg_shuffle`; only the live-across set is spilled, not the whole cache | compile −10.4M; emitted → 479,263; obj −6.4% | +| `f61d252b` | **register-resident call result** — the scalar result is cached (claimed reg, written by the post-call ret move) instead of stored to its home + reloaded | emitted → 474,752; obj → 2.29 MB | + +**Cumulative: compile 2,153M → 2,134M (−18.8M, −0.87%); emitted instructions +526,681 → 474,752 (−9.9%); object 2,501,224 → 2,293,504 B (−8.3%, now 1.09× +tcc); emitted-instruction count 1.54× → 1.39× tcc.** All four byte-deterministic; +green across toy/parse/smoke-x64/smoke-rv64/cg-api/libc(musl+glibc)/opt, sqlite +e2e `84|2` at -O0 and -O1, and a clang-differential call probe (shared-arg- +live-across, >8-arg stack calls, varargs, struct sret/byval, recursion, +fn-pointer dispatch). + +Two lessons: + +- **The emitted-code win (−9.9%) dwarfs the compile-time win (−0.87%).** Removing + a store+reload round-trip is mostly fewer *emitted* bytes; the compile-time + saving is one fewer op emitted. This is squarely the codesize track (§2.1) — + it compounds with emit/objwrite/assemble — and it moved the object to 1.09× + tcc, far more than the per-op levers did. +- **The arg round-trip was the single biggest class** (−10.4M compile, −39.5K + emitted): every call argument was computed into a register, spilled to its + home, and reloaded into the ABI arg register, behind a flush-everything that + also spilled non-live cached locals. + +**Deferred:** lazy transient frame homes (allocate a transient's home only on +its first spill). With coalescing now preventing most spills the eager homes are +benign — they cost frame space + a per-transient `frame_slot`/reclaim call, but +not emitted instructions — and the lazy-home surface (every address builder / +tail-call projection / spill / reclaim / debug-loc reads `->home`) is wide. Low +metric payoff for the risk; revisit only if a re-profile shows `frame_slot` hot. + ## 11. What this is not - **Not Track 4.** This keeps the typed value model and the seam; it does not adopt diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -35,7 +35,7 @@ recent low-load reading. | compiler | instructions | cycles † | wall † | object | |---|--:|--:|--:|--:| | **tcc 0.9.28** | 0.66 B | 0.22 B | 0.06 s | 2.11 MB | -| **kit (current)** | 2.22 B | — | — | 4.26 MB | +| **kit (current)** | 2.13 B | — | — | 2.29 MB | | clang 22 | 8.73 B | 2.57 B | 0.81 s | 1.50 MB | † low-load reading; re-confirm on a quiet machine (`instructions` is @@ -43,9 +43,11 @@ load-independent and the figure to trust; cycles/wall are pending a quiet-machin re-read). kit beats clang on compile speed and is the fastest *general* backend here, but -**tcc is the bar**: ~3.35× instructions ahead. Closing that is the whole game. -(kit's object is larger because `-O0` codegen is deliberately unoptimized — -irrelevant to this goal.) +**tcc is the bar**: ~3.22× instructions ahead. Closing that is the whole game. +(The 2.22 B / 4.26 MB figures earlier in this table's history predate the +codesize track and the -O0 value-stack residency work; current is 2.13 B / +2.29 MB — object now 1.09× tcc, emitted-instruction count 1.39× tcc. See +PERF-TCC-SLIM.md §10c.) ### Reproducing the detailed measurements