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:
| M | doc/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)