kit

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

commit 852e89287976ad5a2b10a2a6ac57ec7d7938b41a
parent f6aa2f2ab094620c0b1ac8bef34ef6b1381c7463
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 14:56:22 -0700

doc(perf): refresh sqlite standings + callgrind profile (PERF.md)

Re-measure on f6aa2f2a (frontend slimming on main + the o0-codesize track):
- kit -c 2.13 -> 1.91 B instr, 3.23x -> 2.88x tcc; object 2.29 -> 1.99 MB (now
  below tcc's 2.11 MB).
- phase split -E 0.97->0.93 B, -fsyntax-only 1.84->1.63 B, emit slice ~0.28 B.
- refresh the Linux callgrind profile (scripts/perf_callgrind.sh, total 3.30 ->
  2.04 -> 1.75 B Ir): add the current self-Ir distribution + subsystem rollup
  (scanner ~24%, types ~14%, pp ~12%, intern ~4%, codegen/emit ~3%). The
  inclusive view surfaces the type subsystem as the #2 cluster — invisible in
  the macOS wall-clock sample, which over-weights lex_next to ~72%.

Diffstat:
Mdoc/plan/PERF.md | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 68 insertions(+), 21 deletions(-)

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.13 B | — | — | 2.29 MB | +| **kit (current)** | 1.91 B | — | — | **1.99 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,13 +43,22 @@ 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.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; object is now ~2.22 MB -(1.09× tcc) after value-stack residency + copy/convert coalescing + lazy -transient frame homes. The last of these is frame-focused: mean -O0 frame --17.6 %, 25 sqlite functions frameless, `sub xNN,x29` frame-addressing -5.3 %. -See PERF-TCC-SLIM.md §10c and §10d.) +**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.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.99 MB +— below tcc's 2.11 MB** (was 2.29 MB), because emitting less machine code also +writes a smaller object. + +The `o0-codesize` track cut emitted `-O0` `.text` from **1.33× → 1.16× tcc** +(455,546 → 398,433 insns) via five landed levers — far-slot positive-scaled +addressing, a 2-insn epilogue, call-result-in-result-register, copy-into-dest +materialization, and narrow-load zero-extend elision. Three are arch-neutral +(also −3.18 % on x86-64). Smaller output is also fewer instructions to emit and +write, so it shrinks the emit+objwrite slice here too. Full standings, diagnosis, +and the remaining levers (args-into-arg-regs, spill reduction): **`doc/plan/ +PERF-O0-CODESIZE.md`**. ### Reproducing the detailed measurements @@ -102,14 +111,16 @@ m build/release/kit cc -c -o /tmp/k.o sqlite3.c --sysroot "$SDK" # + codegen `-fsyntax-only` still drives the full CG value-stack and type lowering (routed to the no-op check backend), so `(-c) − (-fsyntax-only)` isolates **native emit + -object write** only. Current binary: `-E` 0.97 B, `-fsyntax-only` 1.84 B, `-c` -2.13 B → that delta is ~0.29 B / ~14 % — i.e. **codegen+emit is a small slice; -the frontend is the rest.** Within that frontend, `-E` alone (0.97 B) is just +object write** only. Current binary: `-E` 0.93 B, `-fsyntax-only` 1.63 B, `-c` +1.91 B → that delta is ~0.28 B / ~15 % — i.e. **codegen+emit is a small slice; +the frontend is the rest.** Within that frontend, `-E` alone (0.93 B) is just under half, and the scanner (`lex_next`) is the bulk of it — see *Where the time -goes*. (Two landings shrank the emit slice: the scanner rewrite cut a flat ~84 M -off every phase — `-E` 1.06 → 0.98 B / −8.0 % — and the -O0 value-stack residency -work took emit+objwrite 0.37 → 0.29 B / ~7× → ~5.9× tcc by killing the call / -return / assignment home round-trips. See PERF-TCC-SLIM.md §10c.) +goes*. (Landings that shrank these: the scanner rewrite cut a flat ~84 M off +every phase — `-E` 1.06 → 0.93 B — the -O0 value-stack residency work took +emit+objwrite 0.37 → 0.29 B by killing the call/return/assignment home +round-trips, the `o0-codesize` track shaved it again by emitting fewer +instructions, and the frontend slimming on main cut `-fsyntax-only` 1.84 → +1.63 B. See PERF-TCC-SLIM.md §10c and PERF-O0-CODESIZE.md.) **4. Hotspot profile** (self-time per function). One run is far too fast for `sample` (a `-c` is ~0.2 s now), so merge the `Sort by top of stack` sections @@ -200,15 +211,49 @@ Gotchas (each cost a round trip — do not relearn them): parser), printing absurd percentages (`lex_open_mem` at 14,000,000%). The self/exclusive ranking sums cleanly to 100% — use it; read inclusive only as "function X's subtree is hot," not a number. -- callgrind is ~10x slower; the sqlite `-c` collects ~3.3 B `Ir` (more than the - 2.13 B hardware figure — it counts glibc + the loader + the `-lc` probe). The - *distribution* is the point, not the absolute total. +- callgrind is ~10x slower; the sqlite `-c` total (**1.75 B `Ir`**, Linux/ELF) is + not directly comparable to the 1.91 B macOS/Mach-O hardware figure — different + libc, sysroot, and object format, and callgrind also counts glibc + the loader + + the `-lc` probe. The *distribution* is the point, not the absolute total. + +### 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). 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 | +| 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.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.2 | `__GI_memset` | libc | + +Subsystem rollup (kit self `Ir`): **scanner ~24 %, types ~14 %, preprocessor +~12 %, interning ~4 %, codegen/emit ~3 %.** This is the inclusive-instruction +view's payoff: the macOS wall-clock `sample` (next section) shows `lex_next` at +~72 % and the **type subsystem nearly invisible**, but callgrind reveals types as +the **#2 self-`Ir` cluster** — a dozen O(1)-but-frequent per-query functions +(`api_unalias_type`, `api_type_pred_bits`, `cg_type_get`/`cg_type_size`, +`abi_cg_type_info`, `resolve_type`, `api_type_class`, `type_cg_lower`, +`api_type_layout_get`). Codegen/emit is now only ~3 % self (the value-stack +residency + `o0-codesize` work moved it off the hot path), consistent with the +~15 % `-c` − `-fsyntax-only` slice (the rest of that slice is object-write). ## Current state **Real-world compilation is frontend-bound, not codegen-bound.** The phase split -(instructions: `-E` 0.97 B, `-c` 2.13 B) puts native codegen + emit + object-write -at ~14 %; the frontend — preprocessor, lexer, interner, parser, semantic analysis, +(instructions: `-E` 0.93 B, `-c` 1.91 B) puts native codegen + emit + object-write +at ~15 %; the frontend — preprocessor, lexer, interner, parser, semantic analysis, and the CG value-stack/type-lowering it drives — is the rest. (The synthetic `bench-cc` axes below over-weight codegen by construction; trust the sqlite profile for where to spend effort.) @@ -237,7 +282,9 @@ self-time — up from ~66 %, because the value-stack residency work removed the codegen/emit self-time, not because the scanner got slower. **Codegen has dropped off this profile entirely** (the former ~1.6 % cluster — `nd_dst_reg`/ `cg_type_is_aggregate`/… — is now <0.5 %), confirming the emit-phase shrink the -instruction decomposition shows (0.37 → 0.29 B). +instruction decomposition shows (0.37 → 0.28 B, the `o0-codesize` track shaving +it below the value-stack-residency 0.29 B). The callgrind self-`Ir` view (§6) +agrees: `aa_emit_mem` is ~1.1 %, codegen/emit ~3 % total. Three things that used to be hot remain absent: the **type system** (derived-type + ABI/record dedup are O(1)), the **preprocessor hideset** (O(1)