kit

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

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