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