commit 1ff134b534b2d8d026e108221b72abf889db7ab5
parent 8674712d569b75d1bf334314acada9f59eb40ada
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 18:36:36 -0700
doc(perf): refresh PERF.md profiling numbers post-Lever-1 (macOS + Linux callgrind)
Re-measured on the sqlite amalgamation after the o0-codesize Lever 1 landing.
macOS (best-of-7, /usr/bin/time -l): kit `-c` 1.909 B instr (flat vs 1.91 B —
Lever 1 is a codesize lever, compile-instruction-neutral), tcc 0.663 B → ~2.88×;
phases -E 0.93 / -fsyntax-only 1.64 / -c 1.91 B (unchanged). Object refreshed to
the measured 1.83 MB (the prior 1.79 MB figure was stale — the true pre-Lever-1
object was 1.88 MB; Lever 1's -55,320 B .text shrink takes it to 1.83 MB, still
below tcc's 2.11 MB).
Linux callgrind (scripts/perf_callgrind.sh): total 1.746 B Ir (was 1.745 B —
unchanged), same distribution; refreshed the per-function table (aa_emit_mem
1.1→1.0 %; api_sv_adjust_refs surfaces at ~0.8 % from Lever 1's value-stack ref
bookkeeping). Confirms codesize levers shrink emitted code but not compile Ir.
Also updated the o0-codesize one-liner to 1.33×→1.044× tcc (455,546→357,707
insns) and added Lever 1 to the lever list.
Diffstat:
| M | doc/plan/PERF.md | | | 58 | ++++++++++++++++++++++++++++++++-------------------------- |
1 file changed, 32 insertions(+), 26 deletions(-)
diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md
@@ -34,8 +34,8 @@ 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)** | 1.91 B | — | — | **1.79 MB** |
+| **tcc 0.9.28** | 0.66 B | 0.19 B | 0.06 s | 2.11 MB |
+| **kit (current)** | 1.91 B | — | — | **1.83 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,27 +43,31 @@ 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**: ~2.87× instructions ahead. Closing that is the whole game.
+**tcc is the bar**: ~2.88× instructions ahead. Closing that is the whole game.
The gap was 3.23× when this table was last read; two campaigns since closed it to
-2.87×: the frontend instruction-slimming work on main (Sym-keyed binding cache,
+~2.88×: the frontend instruction-slimming work on main (Sym-keyed binding cache,
per-id type decode, single-pass symtab/strtab — `-fsyntax-only` 1.84 → 1.63 B)
-and the **`o0-codesize` track** (next paragraph). kit's **object is now 1.79 MB
-— well below tcc's 2.11 MB** (was 1.99 MB, 2.29 MB before), because emitting less
-machine code also writes a smaller object.
+and the **`o0-codesize` track** (next paragraph). kit's **object is now 1.83 MB
+— below tcc's 2.11 MB** (1.88 MB before Lever 1; 1.99, 2.29 MB earlier), because
+emitting less machine code also writes a smaller object.
-The `o0-codesize` track cut emitted `-O0` `.text` from **1.33× → 1.084× tcc**
-(455,546 → 371,537 insns): five early levers (far-slot positive-scaled
+The `o0-codesize` track cut emitted `-O0` `.text` from **1.33× → 1.044× tcc**
+(455,546 → 357,707 insns): five early levers (far-slot positive-scaled
addressing, a 2-insn epilogue, call-result-in-result-register, copy-into-dest
materialization, narrow-load zero-extend elision), then **Lever 2** (spill
reduction — eager dead-operand drop + a branch materialize-before-flush reorder,
-−24,581) and three single-pass follow-ons (switch-selector residency, signed
-load-with-extend, byte/half far slots; −2,315). Several are arch-neutral (the
+−24,581), three single-pass follow-ons (switch-selector residency, signed
+load-with-extend, byte/half far slots; −2,315), and **Lever 1** (call args
+materialized straight into the ABI arg registers — front the arg/ret regs in the
+-O0 cache pool + arg0-first materialization so producers land args in their slot;
+−13,830, the biggest single lever, all four arches). Several are arch-neutral (the
shared-NDT levers also help x86-64/rv64). Smaller output is fewer instructions to
-emit and write, so it shrinks the emit+objwrite slice — though Lever 2's
-producer-side `api_temp_dead` checks add a small offsetting CG-layer cost, so the
-`-c` compile-instruction total is ~flat (the win shows in the 10 %-smaller
-object). Full standings, diagnosis, and the remaining levers (args-into-arg-regs
-is now the dominant one): **`doc/plan/PERF-O0-CODESIZE.md`**.
+emit and write, so it shrinks the emit+objwrite slice — but each lever's
+producer-side CG cost (Lever 2's `api_temp_dead` checks, Lever 1's arg-order
+reversal + value-stack ref bookkeeping) offsets it, so the `-c`
+compile-instruction total is **~flat (1.91 B, callgrind 1.746 B Ir unchanged)** —
+the win shows in the smaller object and emitted `.text`. Full standings,
+diagnosis, and the remaining levers: **`doc/plan/PERF-O0-CODESIZE.md`**.
### Reproducing the detailed measurements
@@ -225,29 +229,31 @@ Gotchas (each cost a round trip — do not relearn them):
### Current callgrind distribution (self `Ir`, refreshed)
-`scripts/perf_callgrind.sh run` on `f6aa2f2a` (after the frontend slimming on
-main + the `o0-codesize` track). Total **1.75 B `Ir`** (was 3.30 B before the
-strtab fix, 2.04 B after). **Re-confirmed unchanged after the `o0-codesize`
-Lever 2 + single-pass follow-ons** (total 1.745 B; the same distribution, with
-`aa_emit_mem` 1.1 → 1.0 %): the codesize levers shrink emitted *code* but not
+`scripts/perf_callgrind.sh run` (after the frontend slimming on main + the
+`o0-codesize` track). Total **1.75 B `Ir`** (was 3.30 B before the strtab fix,
+2.04 B after). **Re-confirmed unchanged after the `o0-codesize` Lever 2 +
+single-pass follow-ons + Lever 1** (total 1.746 B; the same distribution, with
+`aa_emit_mem` now 1.0 % and a new ~0.8 % `api_sv_adjust_refs` from Lever 1's
+value-stack ref bookkeeping): the codesize levers shrink emitted *code* but not
codegen's *compile-time* `Ir` — that is dominated by per-statement vtable dispatch
-(`nd_dst_reg`/`nd_dst_writeback`/`aa_emit_mem`), and Lever 2's `api_temp_dead`
-checks add back roughly what the smaller emit removed. Per-function self `Ir`
+(`nd_dst_reg`/`nd_dst_writeback`/`aa_emit_mem`), and each lever's producer-side CG
+cost (Lever 2's `api_temp_dead` checks, Lever 1's arg reversal + ref counting)
+adds back roughly what the smaller emit removed. Per-function self `Ir`
(callgrind's `'N` inlined contexts merged):
| self % | function | subsystem |
|--:|---|---|
| **16.9** | `lex_next` | scanner |
-| 4.9 | `src_next_raw_into` | preprocessor |
+| 5.0 | `src_next_raw_into` | preprocessor |
| 4.5 | `lex_point_at` | scanner |
| 4.4 | `pp_pull_into` | preprocessor |
| 4.1 | `pool_intern_slice` | interning |
| 3.1 | `api_unalias_type` | types |
-| 2.7 | `api_type_pred_bits` | types |
+| 2.8 | `api_type_pred_bits` | types |
| 2.0 | `finish_ident` | scanner |
| 1.5 | `resolve_type` / `cg_type_get` (each) | types |
| 1.3 | `cg_type_size` / `abi_cg_type_info` (each) | types |
-| 1.1 | `aa_emit_mem` | codegen |
+| 1.0 | `aa_emit_mem` | codegen |
| 1.2 | `__GI_memset` | libc |
Subsystem rollup (kit self `Ir`): **scanner ~24 %, types ~14 %, preprocessor