kit

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

commit 50893346b1bb4df3b6a568933a6ec7effaf9f04e
parent 635f2c3aa64e3c29f696dc4bec0e64cfcc7c6158
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 11 Jun 2026 12:47:43 -0700

docs(perf): refresh stage breakdown, link breakdown, current ratios

Refresh 'where the time goes' with current self-time data: codegen is
the dominant compile bucket; write() of the .o is newly 8-12% (and is
already optimal — streaming Buf chunks instead of flatten-to-one-write
measured -2.4%, more syscalls; writev is the only marginal lever). Add a
link-time breakdown (content hash ~20%, symbol table/resolve ~20%, mem
~15%, ingest ~12%, emit ~6%; the cheap lever is pre-sizing the symbol
hashmaps). Update the kit/clang ratio table to current measurements.

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

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -84,9 +84,9 @@ kit/clang at the largest measured size (lower = faster): |---|--:|---|--:| | locals-per-fn | 0.01× | type-decl | 0.45× | | ref-density | 0.06× | pp-macro `-E` | 0.32× | -| fn-count | 0.08× | global-decl | 0.31× | -| obj-count (link) | 0.18× | pp-include `-E` | 0.86× | -| body-size | 0.25× | symbol-count (link) | 0.80× | +| fn-count | 0.09× | global-decl | 0.32× | +| obj-count (link) | 0.25× | pp-include `-E` | 0.88× | +| body-size | 0.25× | symbol-count (link) | 0.76× | **Round 6 (constant-factor sprint, gate: `scripts/perf_identity_gate.sh`).** A byte-identical pass across all nine segments cut **+10–13%** off the major @@ -119,24 +119,44 @@ link axes (its output hashing uses the ARMv8 SHA extension). ### Where the time goes (self-time by stage, `scripts/cc_bench_stages.py`) -The bottleneck is now **codegen** on the per-statement axes and the **parser** on -`type-decl`; the token pump, `memset`, and IO that used to lead are cut. Self-time -% by stage (each axis isolates one input dimension): - -| stage | body-size | locals | ref-density | fn-count | type-decl | pp-macro `-E` | -|---|--:|--:|--:|--:|--:|--:| -| **codegen** (`nd_*` regcache, `api_*`, `cg_type_get`) | **42** | **31** | **27** | 19 | – | – | -| **parser** (`parse_decl_specs`/`_declarator`, `reject_redef`) | 12 | 15 | 13 | 14 | **33** | – | -| **emit** (`buf_write`, `aa_emit_*`, `obj_*`) | 9 | 8 | **18** | 12 | – | – | -| **lexer** (`lex_next`, `scan_pp_number`) | 7 | 9 | 5 | 6 | 17 | 9 | -| **pp** (`pp_next_raw`, `src_next_raw`, `subst_phase2`) | 5 | 5 | 6 | 7 | 15 | **62** | -| **intern** (`pool_intern_slice`) | 3 | 7 | 6 | 10 | 10 | 13 | -| **types/abi** (`c_abi_*`, `type_cg_*`) | 6 | 6 | 3 | 5 | 2 | – | -| **libc memset/memmove** | 10 | 13 | 13 | **16** | 8 | 13 | -| **libc io/vm** (`write`) | 3 | 1 | 4 | 4 | – | – | - -(`pp-macro -E` is preprocess-only, so its 62 % pp is the irreducible token-pump + -substitution that *is* `-E`'s work.) +Current self-time % by stage (post Round-6; `build/bench/kit`, well-sampled axes): + +| stage | body-size | locals | fn-count | pp-macro `-E` | top frames | +|---|--:|--:|--:|--:|---| +| **codegen** | **38** | **35** | 19 | – | `nd_dst_reg` (regcache), `cg_type_get`/`api_unalias_type`, `nd_binop` | +| **parser** | 9 | 12 | 14 | – | `declare_function`, `scope_define_checked`, `parse_decl_specs` | +| **libc mem** (memset/memmove) | 9 | 12 | 12 | 9 | per-fn/node zeroing + payload copies | +| **libc io** (`write`) | 8 | 8 | **12** | 2 | **writing the `.o` output** | +| **lexer** (`lex_next`) | 8 | 5 | 6 | **12** | the scanner | +| **pp** (`pp_next_raw_into`, `subst_phase2`) | 5 | 5 | 5 | **66** | the token pump (the 66% *is* `-E`'s work) | +| **intern** (`pool_intern_slice`) | 3 | 6 | 9 | 8 | identifier interning | +| **emit** (`obj_symbol_find`, `aa_emit_*`) | 6 | 5 | 12 | – | per-instruction emit | +| **types/abi** | 7 | 5 | 3 | – | `c_abi_*`, `type_cg_*` | + +**Codegen is the dominant bucket** — the `-O0` regcache scan (`nd_dst_reg`) and the +per-operand type queries (`cg_type_get`/`api_unalias_type`) are the top frames, and +only copy-and-patch (above) would structurally lower them. Newly notable: +**`write()` of the object file is 8–12%** — as the compute shrank ~10–15%, the +output-write became a real fraction. It is *not* an easy lever: the writer is +already buffered and flattens each section into **one big `write()`** (minimal +syscalls); streaming the section's `Buf` chunks instead measured **−2.4 %** +(N×64 KB syscalls beat one big write only in theory). The visible `write%` is +mostly irreducible kernel I/O; the only further lever is `writev` (one +scatter-gather syscall, no flatten copy) — marginal (~0.2–0.5 %) and needs a new +writer vtable method. **`pp-include` is ~95 % `open`+`read` syscalls** (header-cache +territory, helps real re-include graphs not the single-include axis). + +### Where link time goes (flat self-time, forced 1M-symbol link ≈ 1 s) + +The linker won't sample at normal bench sizes (sub-ms/unit; beats ld64). At 1M +symbols, no single hotspot: **content hash `sha256_update` ~20 %** (the HW-SHA +build-id/UUID over all output bytes — irreducible: a deterministic build-id must +hash the whole image), **symbol table + resolve ~20 %** (`obj_symbol_make`, +`link_resolve_symbols`, `link_assign_symbol_vaddrs`), **mem ~15 %** +(`__bzero`/memset/memmove), **object ingest ~12 %** (`link_ingest_archives`, +`read_macho`), **output emit ~6 %** (`link_emit_macho`, relocations), **intern + +hashmap resize ~6 %**. The one cheap link lever left is pre-sizing the symbol +hashmaps (`SymNameIndex_resize`/`SymHash_resize`, ~3 %). ### Invariants (hold these)