kit

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

commit f8f4e3001e5e7b7283e99b8b0e2209b738c167f0
parent 277edfa3d286c43fc3e80ffc1f0ad0c5795321d3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 08:24:51 -0700

doc/plan/PERF.md: sqlite -O1 optimizer callgrind profile (opt_regalloc_place 24.5%); from disasm-audit run, artifacts in build/linux-prof/

Diffstat:
Mdoc/plan/PERF.md | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+), 0 deletions(-)

diff --git a/doc/plan/PERF.md b/doc/plan/PERF.md @@ -246,6 +246,51 @@ instruction metric (Apple malloc is cheap) but it corrects the reset-vs-free semantics and cuts heap pressure/fragmentation. Heap counters reuse the `KitProfiler` machinery (embedder counter range), opt-in via `KIT_METRICS=1`. +**SQLite `-O1` optimizer profile** (Linux callgrind, self `Ir`, +optimizer-finalize only). Full sqlite `-O1` callgrind collection on the default +2 GiB Podman VM overflows Valgrind's brk segment and dies with rc 137, so the +useful profile toggles collection at `opt_on_finalize` and forces glibc malloc +onto `mmap` (`MALLOC_MMAP_THRESHOLD_=1 MALLOC_ARENA_MAX=1`). That captures the +whole-program O1 sweep (reachability/internalization, CGIR lowering, inlining, +per-function O1 pipeline, native emit) but **excludes frontend recording**, so it +is a distribution profile, not a total comparable to the `-O0` full-compile +profile above. Command and raw output are in +`build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.*`. + +Profiled command: +`kit cc -O1 -c sqlite3.c -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -lc`. +Total collected inside `opt_on_finalize`: **15.496 B Ir**. + +| self % | function(s) | subsystem | +|--:|---|---| +| **24.5** | `opt_regalloc_place` | physical register placement | +| **18.7** | `opt_func_from_cg_ir` | CGIR -> optimizer Func lowering | +| **7.8** | `alloc_assign_group_hard` | hard-register assignment grouping | +| **7.4** | `ir_note_emit` | IR/lowering record construction | +| 4.5 | `opt_build_loop_tree` | loop analysis | +| 3.2 / 1.7 / 1.6 / 1.5 | `opt_bitset_has` / `opt_bitset_union` / `opt_bitset_copy` / `opt_bitset_union_and_not` | liveness/range bitsets | +| 2.4 | `opt_live_ranges_build` | live-range construction | +| 2.1 / 0.8 | `msort_with_tmp` / `u32_cmp` | sorting in optimizer hot paths | +| 1.6 / 0.3 | `__GI_memset` / `__GI_memcpy` | libc memory | +| 1.5 / 0.2 | `inline_call_site` / `opt_inline` | whole-program inliner | +| 1.2 / 1.0 / 0.3 / 0.2 | `metrics_count` / `metrics_scope_from_name` / `metrics_scope_from_name'2` / `metrics_count'2` | profiling string-name lookup | +| 1.1 | `arena_alloc` | arena bump path | +| 1.0 | `opt_addr_xform_pregs` | address transform | +| 0.1 | `opt_emit_native` | native emit self-time | + +Interpretation: sqlite `-O1` compile time is dominated by the optimizer's +middle/back half, not native emission. Register allocation alone is at least +**32 %** (`opt_regalloc_place` + `alloc_assign_group_hard`, excluding the shared +bitset/range helpers), while CGIR lowering plus record construction is another +**26 %**. Built-in metrics on the same TU report **2,522 functions**, +**121,075 blocks**, **98,516 PRegs**, **1,049,079 ranges**, and **29.1 M live +bitset words touched**, which explains why the liveness/range bitset helpers are +visible. A cheap-looking cleanup also shows up: `metrics_scope_from_name` and +`metrics_count` burn ~2.7 % of O1 finalization even with `KIT_METRICS` unset, +because each hot scope/counter call string-decodes the name before discovering +the profiler sink is null. The O1 hot path should use enum-specific wrappers or +check the profiler before name decoding. + ### Code size is still locally spill-bound Even with aggregate `.text` now smaller than tcc, the remaining per-mnemonic @@ -325,6 +370,33 @@ scripts/perf_callgrind.sh run <tag> # builds kit in-container, runs callgrind, # → build/linux-prof/cg.<tag>.annot.txt (self Ir + callers) ``` +For the sqlite `-O1` optimizer-finalize profile, use `podman machine` explicitly +to give the VM enough memory for Valgrind, and force malloc away from brk: + +```sh +podman machine stop || true +podman machine set --memory 3072 +podman machine start +podman run --rm --platform linux/arm64 -v "$PWD":/work:Z kit-prof sh -c ' + set -eu + cd /work/tmp/projects/sqlite-amalg + env MALLOC_MMAP_THRESHOLD_=1 MALLOC_ARENA_MAX=1 \ + valgrind --tool=callgrind --cache-sim=no --branch-sim=no --dump-instr=no \ + --collect-jumps=no --collect-atstart=no --toggle-collect=opt_on_finalize \ + --callgrind-out-file=/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.out \ + /work/build/linux-prof/kit cc -O1 -c sqlite3.c \ + -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -lc -o /tmp/sqlite_o1.o \ + 2>/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.vg.log + callgrind_annotate --threshold=99.5 \ + /work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.out \ + >/work/build/linux-prof/cg.sqlite_o1.opt_finalize.mmap.annot.txt +' +# restore the default local VM shape after the run +podman machine stop +podman machine set --memory 2048 +podman machine start +``` + Gotchas baked into the script (do not relearn): **(a)** pass `-lc` so kit finds the glibc sysroot even for `-c`; use **bookworm** (glibc 2.36), not ubuntu 24.04 (glibc 2.39's `bits/math-vector.h` uses a vector-typedef attribute the C frontend