kit

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

commit edc69e06f3adb3e28162278281e22517db860d06
parent 01aaff143e7d8d45b0bcb7a4bae87a05c5917211
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 07:38:59 -0700

doc/plan/O1-PATTERNS.md: kit-vs-clang -O1 disasm audit — residual linear-budget opportunities (L1-L10)

Diffstat:
Adoc/plan/O1-PATTERNS.md | 535+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdoc/plan/O1.md | 5++++-
2 files changed, 539 insertions(+), 1 deletion(-)

diff --git a/doc/plan/O1-PATTERNS.md b/doc/plan/O1-PATTERNS.md @@ -0,0 +1,535 @@ +# O1 code-quality patterns: the post-W1 disassembly catalog + +A curated catalog of the *residual* `-O1` vs clang `-O1` code-quality gaps, from a +per-file disassembly audit of the ecosystem corpus **after the entire O1.md §3 +worklist (W1–W10) landed** (2026-06-16, aggregate `__TEXT` ≈ 1.09× clang). This +is the successor to that worklist: it identifies what remains, ranks the +linear/no-SSA wins still on the table, and cleanly separates them from the gaps +that genuinely require the parked O2 SSA mid-end. + +Read [O1.md](O1.md) first — especially §3 (the landed worklist), §4 (the SSA/O2 +out-of-scope list), and §5 (already-landed). Read [OPT.md](OPT.md) §3 for the O1 +pipeline and pass inventory. Every "linear" claim here is checked against those. + +--- + +## 1. Methodology + +**Harness.** `scripts/o1_quality.sh` builds the per-file kit (`.k.o`) and clang +(`.c.o`) objects into `build/o1_quality/`; disassemble with +`./build/release/kit objdump -d NAME.k.o`. Per-file ratio = kit `__TEXT` ÷ clang +`__TEXT` (`size -m`). Instruction counts are real instruction lines (objdump +format is `addr:\t bytes \t mnemonic operands` — the mnemonic is after the +*second* tab; a naive `grep ':\t\w+'` matches the byte column and is wrong). + +**Corpus and current standings** (kit / clang `__TEXT`, all aarch64/Darwin `-O1`, +release kit): + +| file | kit/clang | note | +|---------|----------:|------| +| lz4 | **0.38×** | clang unrolls/inlines → clang bigger (NOT a kit gap) | +| yyjson | 0.86× | kit smaller (clang inlines far more) | +| lapi | 0.83× | kit smaller | +| tinyexpr| 0.96× | ~par | +| lparser | 1.07× | ~par; the SIZE_MAX/udiv guard pushes it over | +| cjson | 1.31× | aggregate/array zero-init dominates | +| miniz | 1.67× | csel + frame traffic in the deflate loops | +| sqlite | 1.11× | frame traffic + bit-tests + addr folds | +| lvm | **2.86×** | the lua interpreter loop — still the worst | + +W1.1 is confirmed landed in these objects: `sub xN,x29,#k` spill-address +recomputes are **0** in lvm (was 8,774) and sqlite (was 35,360); every spill slot +is a one-instruction positive `ldr/str [x29,#k]`. So the residual gap is no longer +*spill addressing* — it is the **spilling itself** (frame round-trips that a real +RA would not emit) plus a family of local peepholes the no-SSA O1 still misses. + +**Rubric.** Each pattern is graded LINEAR (a same-block / per-instruction forward +peephole, no SSA, no superlinear axis) · NEEDS-SSA-O2 (wants GVN/DSE/LICM/IV or a +cross-block register allocator) · NOT-A-DEFICIENCY (clang bigger via inlining/ +unrolling, or kit already smaller). Linearity verdicts and host-pass claims were +spot-checked against the live passes (`pass_combine.c:1914` compact rule, +`aa_set_bytes` at `aa64/native.c:2577`, `aa_add_lsl`/`aa_ldst_regoff` emitters). + +**Spot-checks performed** (all reproduced against the current `build/o1_quality/` +objects): lvm store→reload-different-register (1495 adjacent + 1962 within-4), +`mov wN,wM` 330 vs clang 5; cjson `strb` 338 with a 64-long consecutive run; +yyjson double-cset 573 triples and cset→cbnz 438. No high-impact pattern was +dropped on spot-check (one was *down*graded — see §4). + +--- + +## 2. Linear / n-log-n O1 opportunities + +The actionable shortlist, ranked by (impact × confidence × cross-file breadth). +This is the section that matters. + +### L1 — Store-to-load forwarding across a register mismatch ★ highest leverage + +**The gap.** After a spilled def is homed (`str rX,[slot]`) the O1 reload at the +next use targets a *fresh rotating scratch* (`ldr rY,[slot]`, `rY != rX`), so the +value round-trips through memory even though `rX` still holds it. The existing +adjacent compaction (`opt_combine_compact_block`, `pass_combine.c:1934`) collapses +the `str;ldr` pair **only when `same_reg_operand(store.src, load.dst)`** (rX==rY); +the dominant different-register case falls straight through to `bl->insts[w++]` +and survives. + +``` +kit (lvm _luaV_execute): clang keeps the bytecode word live: + str w10, [x29, #512] lsr x8, x28, #7 + ldr w9, [x29, #512] ; w9 != w10 add x8, x21, x8, lsl #4 + and w10, w9, #0xff ubfx w9, w28, #7, #8 + str w10, [x29, #512] ldr x10, [x9] + ldr w9, [x29, #512] ; again (no stack round-trip at all) +``` + +**Where / frequency.** lvm: **1495** adjacent `str;ldr` same-slot different-reg +(vs 79 same-reg already handled), 1962 within a 4-insn window; concentrated in +`luaV_execute`. lz4: 191 intra-block store-then-adjacent-reload pairs (the two +giant funcs). sqlite: ~8,500 store-then-reload-within-4. miniz: 1103 near same-slot +store→reload. **The single biggest cross-file contributor to the ldr/str excess** +(kit lvm ldr 4709 vs clang 731, str 3813 vs 214). + +**Impact.** Each forwarded reload becomes a `mov rY,rX`, which the existing copy +substitution then folds into rY's uses (often deleting the `mov`); when rX is dead +and the slot has no other reader, the store is dead too (→ W8 stack-DSE + mir_dce). +Conservatively ~1400 insns in lvm alone (~9% of the object), with real wins in lz4 +(~380) / sqlite (intra-block subset) / miniz. + +**Host pass + sketch.** `pass_combine.c`, `opt_combine_compact_block`. In the +`prev==IR_STORE && in==IR_LOAD && same_spill_slot_and_size` branch, drop the +`same_reg_operand` requirement: when the registers differ, rewrite the load +in-place to an `IR_COPY rY <- rX` instead of keeping it as a memory load. The +existing copy substitution / `mir_dce` retire the copy and (if rX dead + slot +unread) the store. The store-DSE half is W8, already present. + +**Stays linear:** single forward pass per block with the bounded last-store +tracking the compact pass already does; no new analysis, no cross-block reasoning. +Correctness boundary: only when no intervening clobber of rX, no aliasing memory +write, and no redefinition of the slot between str and ldr — exactly the adjacency +the compact pass already guarantees for the same-reg case. + +> Note vs O1.md: this is the natural extension of the **W8** machinery, not a new +> pass. W8 today finds 0 sites because W2/DCE clear the store-store shape upstream; +> the store→load-forward-on-mismatch shape is a *different* and very live pattern +> that no landed item touches. + +### L2 — Fuse `cmp; cset rD,cc; cbnz/cbz rD` → `cmp; b.cc` (boolean-into-branch) ★ broadest + +**The gap.** For `if (relational) goto L`, kit materializes the condition into a +GPR (`cset rD,cc`) and then re-tests it (`cbnz/cbz rD`), where clang branches +directly off the flags. The original `cmp`'s NZCV already encodes the branch; the +`cset` is the sole user and dies at the branch. + +``` +kit (yyjson _yyjson_arr_iter_init): clang: + cmp w8, #0 cmp w8, #0 + cset w12, eq b.eq 0x604 (or cbz w8,0x604) + cbnz w12, 0x604 +``` + +**Where / frequency — pervasive.** yyjson **438**, lvm 19 (→54 once L1 removes the +intervening homing), lapi 27, lparser 5, lz4 10, sqlite (part of the cbnz +2,440 +delta). clang emits ~0 of these. This is the most cross-file pattern in the +catalog. + +**Impact.** ~2 insns per site where a redundant cmp-against-0 also precedes the +cset (yyjson's "double-cset", see L4), else 1 insn (the cset). yyjson ~880 insns +(~2.1% of `__TEXT`); lapi ~27; broad small wins elsewhere. + +**Host pass + sketch.** `pass_combine.c` (or `pass_jump.c`, which already owns +`IR_CMP_BRANCH`/`IR_CONDBR` terminators). When a block terminator is `IR_CONDBR` +whose condition register's most-recent same-block def is a single-use `IR_CMP`, +fuse into `IR_CMP_BRANCH` carrying the cmp's `CmpOp`+operands (invert for the +cbz/`==0` case) and NOP the `IR_CMP`. This is the inverse of the fusion +`cg_ir_lower.c:941` already performs in the other direction; reuse the `CombineCtx` +`last_def` map + the existing hard-live use counts. + +**Stays linear:** same-block last-def lookup + single-use check, both already +computed; no SSA. + +### L3 — Widen aggregate/`memset` zero-init (one `strb` per byte → wide stores) ★ cjson/miniz + +**The gap.** `aa_set_bytes` (the AGG_SET / memset expander, `aa64/native.c:2577`) +emits exactly one `strb` per byte in a flat `0..size` loop, even for an aligned +whole-struct/array zero. A 64-byte zero is 64 `strb`; clang does it in 3–4 +(`movi v0.16b,#0; stp q0,q0,...`) or, scalarized, `str xzr` runs. + +``` +kit (cjson _cJSON_PrintPreallocated): clang: + add x8, x29, #16 movi v0.16b, #0 + movz x9, 0x0 stp q0, q0, [sp, #32] + strb w9, [x8] stp q0, q0, [sp] + strb w9, [x8, #1] ; 64 bytes in 3 insns + ... (64 consecutive strb) +``` + +**Where / frequency.** cjson: **338 `strb`, a 64-long consecutive run** confirmed +(296 of 338 are zero-byte stores from this path; clang has 30 strb total). +Hottest in `_cJSON_PrintPreallocated`/`_PrintBuffered` (64 each) and `_print_number` +(26). miniz: large `strb` surplus from PNG/struct buffers. + +**Impact.** Largest single win in cjson — ~250–300 insns / ~25% of cjson's 1236- +insn gap. Even a scalar widening (`str xzr` for 8-aligned runs, `str wzr`/`strh +wzr`/`strb wzr` tail) without NEON captures most of it. + +**Host pass + sketch.** `aa64/native.c` `aa_set_bytes`: rewrite the per-byte loop +to emit the widest aligned store covering the remaining run — `stp q`/`str q` if +NEON is permitted, else `str xzr`, `str wzr`, `strh wzr`, `strb wzr` tail; or `bl +_memset` above a size threshold. Pure per-call expansion, no analysis. The hardware +zero register is already used for stored zeros (`pass_native_emit.c`). + +**Stays linear:** it *removes* emitted instructions from a single expander; no new +pass. (rv64/x64 set_bytes should get the same widening for parity, but aa64 is the +measured win.) + +### L4 — Drop the redundant boolean re-normalize (`cset; cmp rD,#0; cset`) — yyjson + +**The gap.** kit lowers `(a==b)` to `cmp; cset rD,eq` (a 0/1 bool), then a +surrounding `bool != 0` / `if(bool)` context lowers to a **second** `cmp rD,#0; +cset rD,ne` that re-normalizes an already-0/1 value. The outer cmp+cset are a +no-op. + +``` +kit (yyjson _unsafe_yyjson_is_raw): clang (at each inline site): + cmp w8, #1 cmp w8, #1 + cset w8, eq cset w0, eq + cmp w8, #0 ; redundant + cset w8, ne ; w8 == w8 +``` + +**Where / frequency.** yyjson: **573** exact `cset; cmp #0; cset` triples (0 in +clang); dense in the `is_*`/`get_*` predicate family. (lapi/sqlite have the related +single-cset-of-a-bool but the triple is yyjson's signature.) + +**Impact.** ~2 insns each → ~1100 insns / ~2.6% of yyjson `__TEXT`. Combines with +L2: many of these feed a branch, so the L2 fusion subsumes the outer half. + +**Host pass + sketch.** `pass_combine.c` same-block forward peephole: when an +`IR_CMP`'s source operand's most-recent same-block def is itself an `IR_CMP` +(0/1 bool) and the comparison is against imm 0/1 with eq/ne, drop the outer +`IR_CMP` and rewrite its uses to the inner result (inverting for the `#0`/ne case). +Uses the existing `last_def` map + hard-live use counts. + +**Stays linear:** single-block, single-use, checkable from existing state. + +### L5 — Single-use register copy not coalesced into its consumer + +**The gap.** kit produces a value into one register then `mov`s it into the +register its sole consumer (a `sxtw`/store/call-arg/return) reads, instead of +producing directly into the final register. The cross-block coalescer W3 cannot +retire these because the destinations are multiply-defined physical pregs (W3's +`opt_copy_cleanup` only fires when `ndef[dst]==1`); the per-block `mir_combine` +substitution doesn't reach all the consumer paths. + +``` +kit (lvm): kit (lapi return): + ldrb w13, [x8, #11] and w12, w12, #0x7 + mov w12, w13 ; redundant mov w0, w12 ; redundant + sxtw x19, w12 ; sxtw x19,w13 ok ret ; and w0,w8,#0x7 ok +``` + +**Where / frequency.** Reg-to-reg `mov wN,wM`: lvm 330 vs clang 5; lapi 512 vs 194; +lparser 948 vs 541; miniz 1018 of 3203 are mov-of-prev-insn-dest; yyjson 209 feed a +uxtb + 59 are return copies (`mov w0,wN` before ret). The single largest `mov` +contributor across the value-heavy files. + +**Impact.** Several hundred insns across lapi+lparser+miniz+yyjson+lvm (conservative +after excluding genuinely class-crossing or still-live moves). + +**Host pass + sketch.** `pass_combine.c` post-RA peephole using the existing +`CombineCtx` liveness (`ctx_def_changed_since`, `count_uses_in_live_range`): for +`mov rD,rS` where rD has a single use in its live range before its next redef and +rS is not redefined between the mov and that use, forward rS to the use and drop +the mov. Extend the consumer set beyond what `try_substitute`/`try_sink` reach +today: convert/extend, store-value, call-arg, and `IR_RET` value operands. The +`IR_RET`→return-reg back-propagation mirrors the existing redundant-second-copy +handling at `pass_combine.c:~890`. + +**Stays linear:** the `CombineCtx` machinery already answers both guards; same-block. + +### L6 — Drop the redundant `sxtb`/`sxtw` after a same-width extending load + +**The gap.** Two shapes. (a) yyjson loads a signed byte as `ldrsb xD,[addr]` (full +64-bit sign-extend) then emits `sxtb wE,xD` for a W-domain consumer — but xD's low +32 bits are *already* the sign-extended byte, so the `sxtb` is a no-op (and clang +just does `ldrsb wD`). (b) lvm/sqlite `sxtw` of a value sourced from a zero-extending +`ldrb`/`ldrh`: kit loads always zero-extend, so a 0..255 byte widens identically +signed or unsigned → the `sxtw` is a same-width copy. + +``` +kit (yyjson _yyjson_mut_ptr_getx): clang: + ldrsb x12, [x21] ldrsb w9, [x13] + sxtb w8, x12 ; x12<31:0> == byte cmp w9, #34 + cmp w8, #47 +``` + +**Where / frequency.** yyjson: 112 `ldrsb x..; sxtb w..` + 74 `ldrb; sxtb`. lvm: +the `ldrb;mov;sxtw` triple subset (~6 tight + more across the body). sqlite: part of +the sxtw +2,582 (the ldrb/ldrh-rooted subset). This is the signed mirror of the +already-landed ZEXT-of-load fold (OPT.md §3 / commit 8826982d). + +**Impact.** yyjson ~186 insns; lvm ~30–80 from the byte-rooted cases; sqlite a slice +of the sxtw delta. Medium confidence on the broad count, high on the exact-shape +subset. + +**Host pass + sketch.** `pass_combine.c` `combine_exts` (already folds ext-of-ext): +a `SXTB`/`SXTH`/`SXTW` whose source's most-recent same-block def is a sign-extending +load of equal-or-smaller width, *or* a zero-extending narrow load whose value can't +have the sign bit set in the widened position, is redundant → drop it (rewrite uses +to the load dst). Folding `ldrb;sxtb`→`ldrsb` needs a load-with-extend MemAccess +rider (noted as structurally needed in prior O0 work); the pure +`sxtb`-after-`ldrsb`/`sxtw`-after-`ldrb` *drop* needs no rider and is the easy win. + +**Stays linear:** per-instruction, same-block last-def; no SSA. + +### L7 — Fold a single-use shift into the consuming ALU op (`lsl;add` → `add ...,lsl #k`) + +**The gap.** kit folds shifts into *memory* operands (`try_addr_synth` handles ISHL +into an indirect index scale) but not into general ALU consumers. So pointer +scaling whose sum is stored/compared (not a load address) emits `lsl xT,xS,#k; add +xD,xB,xT` where clang uses the one-instruction shifted-register form. + +``` +kit (lapi _index2value): clang: + sxtw x12, w1 sxtw x21, w1 ; once + lsl x12, x12, #4 add x8, x8, x21, lsl #4 + add x8, x13, x12 +``` + +**Where / frequency.** lvm 14 `lsl;add` (+`lsl;sub`); miniz 105 `lsl`-feeds- +add/orr/eor/sub (clang folds 74); lapi ~15–25; sqlite part of the lsl +1,335 delta. +kit emits ~0 shifted-register ALU ops; clang uses them heavily. Strongest in miniz's +LZ hash (`eor ...,lsl #5`) and byte-pack code. + +**Impact.** miniz ~105 insns; lvm/lapi ~20–30 each; plus a freed scratch that +relieves the spill pressure feeding L1. + +**Host pass + sketch.** `pass_combine.c` recognition + `aa64/native.c` emission. +Mark a single-use `IR_BINOP SHL reg,imm` (sh 1..4) feeding an `IADD`/`ISUB`/AND/ORR/ +EOR and emit the fused shifted-register form. The emitter exists (`aa_add_lsl`, +`native.c:750`; the ISA exposes `AA64_FMT_LOG_SR`/`ADDSUB_SR`) — it just isn't +reachable for non-memory consumers. Needs a shift rider on the register operand (the +current `Operand` only carries `log2_scale` inside the `ind` memory variant). x64 can +use this only for IADD (LEA scale); rv64 has no shifted-add → skip there. + +**Stays linear:** local single-use peephole, same live-range guards as the existing +ISHL-into-EA path. Medium confidence (needs the operand-model shift-rider). + +### L8 — Fold a `sxtw`/`uxtw` index into the load/store addressing mode + +**The gap.** For signed-int array indexing kit emits `sxtw` to widen the index, a +separate `add ...,lsl #scale` to form the address, then a plain `[reg]` access — +three instructions. aa64 has `[Xbase, Wm, sxtw #scale]` as a single addressing mode +(`aa_ldst_regoff`, `native.c:600`); clang uses it everywhere, kit zero times. + +``` +kit (miniz): clang: + sxtw x14, w12 ldr w8, [x9, w8, sxtw #2] + add x9, x0, x14, lsl #2 strh w9, [x26, w13, sxtw #1] + ldrh w20, [x9] +``` + +**Where / frequency.** miniz 15 `sxtw;add...,lsl` feeding a load/store (clang 98 +extended-reg memory operands, kit 0). sqlite **546** `sxtw` immediately feeding a +load/store offset. lapi/lparser index math. Big total across sqlite+miniz. + +**Impact.** sqlite ~546 insns (the standalone sxtw folds into the access); miniz +~100–200 (removes sxtw + the address-forming add). LINEAR slice of the larger +sxtw story (the cross-block redundant *re*-extend is NEEDS-SSA-O2, see §3). + +**Host pass + sketch.** `pass_addr_fold.c` already builds `OPK_INDIRECT` with +index + `log2_scale` but does not absorb a `sxtw`/`uxtw` of the index. Add an +extend-into-index rule: fold a single-use `sxtw`/`uxtw` producer of an index operand +into the indirect operand, recording the extend kind, and emit `aa_ldst_regoff_v` +with the extend in `aa64/native.c`. The scaled-index infra + REGOFF encoding exist. + +**Stays linear:** per-instruction forward fold, existing addr-fold machinery. + +### L9 — Constant-operand integer divide/multiply not folded + +**The gap.** kit materializes two constants and issues a runtime `udiv`/`sdiv`/`mul` +for fully compile-time-constant expressions (classically `ArraySize = sizeof/elem`, +and the Lua `MAX_SIZET/sizeof(T) >= LIMIT` growvector guard). The guard's SIZE_MAX +takes a 4-insn `movz/movk` chain into a register, then `udiv` + `cmp` + `b.hi`; +clang decides the always-true compare and materializes only the `LIMIT` immediate. + +``` +kit (sqlite _sqlite3_status64): clang: + movz x9, 0x50 cmp w0, #0xa + movz x10, 0x8 + udiv x8, x9, x10 ; = 10 at compile time + cmp w20, w8 +``` + +**Where / frequency.** sqlite 108 div/mul sites with both operands freshly `movz`'d. +lparser **22 udiv** (12 udiv-by-1, 10 the SIZE_MAX guard); lapi 9 sdiv; tinyexpr 5. +The SIZE_MAX guard is the dominant reason lparser is *larger* than clang. + +**Impact.** sqlite ~300–400 insns (each site collapses movz+movz+udiv+mov). lparser +~80–90 (the guard is ~7–8 insns → 1 movz at ~10 sites + the udiv-by-1). Each udiv is +also the slowest integer op, so a latency win too. + +**Host pass + sketch.** Two pieces. (a) `pass_simplify.c`: add a same-block forward +`LOAD_IMM`→known-value tracker (per-block reg→value map, invalidated on redef/block +boundary — no SSA, consecutive within-block) and feed it into `operand_const` so the +existing `BO_UDIV`/`BO_SDIV` folds and a new const-op-const fold fire when the +constant lives in a register rather than an inline `OPK_IMM`. The resulting `LOAD_IMM` +then lets `simplify_cmp` decide the always-true guard. (b) The divide-by-1 → +copy/sub falls out of (a). + +**Stays linear:** a bounded per-block reg map + the existing folds. Strength-reduction +of *non-constant* divide-by-constant (magic multiply, miniz adler32) is a separate, +also-linear `pass_lower.c` item but lower priority (~25 sites, medium confidence). + +### L10 — Frame elision on no-spill leaf functions + +**The gap.** kit emits a full `stp x29,x30,[sp,#-16]!; add x29,sp,#0 ... ldp` frame +in functions that are leaves (no call), spill nothing, and never reference `[x29]` — +i.e. need no frame. clang omits it entirely; for a one-op accessor the frame is the +majority of the function. + +``` +kit (tinyexpr _add): clang: + stp x29, x30, [sp, #-16]! fadd d0, d0, d1 + add x29, sp, #0 ret + fadd d0, d0, d1 + ldp x29, x30, [sp], #16 + ret +``` + +**Where / frequency.** yyjson 90 frameless-able leaves; lapi ~21; lparser ~10; +tinyexpr 8. The `is_*`/`get_*`/cast accessor family. (Caveat: many of these are +exactly the predicates clang *inlines away*, so the realizable win depends on +whether kit also inlines them — the frame-elision is independently correct for the +leaves that remain.) + +**Impact.** ~3 insns/function (stp+add+ldp). yyjson ~360, lapi ~63, lparser ~30, +tinyexpr ~24. Medium confidence on realizable total (the inlining caveat). + +**Host pass + sketch.** `aa64/native.c` prologue/epilogue (NativeFrame): a +per-function `needs_frame` flag = false when no `IR_CALL`, no spill slots, no +escaping `IR_ADDR_OF(local)`, no varargs. All known at frame-finalize time in one +pass; emit the stp/add/ldp only when set. x29 already sits at frame bottom (W1.1), so +omission is straightforward. + +**Stays linear:** single per-function check, no analysis. + +--- + +## 3. Needs SSA / the O2 mid-end + +Real gaps, but none is a linear no-SSA peephole — each maps to a parked O2 item +(OPT.md §3 schedule, `pass_o2.c`/`pass_ssa.c`; O1.md §4 out-of-scope list). + +- **Register-resident locals across blocks/calls (the over-spilling itself).** The + *largest* remaining gap on the big functions: kit's frame traffic is ~6× clang's + on sqlite (62,708 vs 10,412), ~4.3× on lz4, dominant in lvm/miniz. clang keeps + loop-carried accumulators/pointers in callee-saved regs across the whole live + range; kit's no-SSA linear-scan allocator (no splitting, falls to memory residency + in big bodies) cannot. **L1 (store→load forwarding) is the linear slice that + reaches the intra-block subset; the cross-block residency needs the SSA RA with + live-range splitting + global copy propagation.** → O2 register allocator + GVN. +- **csel / csinc / ccmp selection (if-conversion).** kit emits **0** csel/csinc/ccmp; + clang uses them heavily (lvm 72+19, miniz 218+34, sqlite 1,264+436, cjson 32, lz4 + 499). Every `x = cond ? a : b`, MZ_MIN/MAX clamp, and double→int saturate becomes a + compare+branch diamond with the selected value threaded through the frame (feeding + the spill gap). Doing it well needs a select op in the IR + a profitability/legality + gate that proves both arms side-effect-free and reasons about the merge of two + definitions — the SSA/O2 domain. (A narrow peephole for the exact + `cmp;b.cc;mov-imm;b;mov-imm` diamond could fit `pass_jump.c` but is fragile and + lower-value than the §2 items — *flag, do not prioritize*.) → O2 if-conversion. +- **`tbz`/`tbnz` from `and #(1<<k); cbz/cbnz`.** sqlite has 715 `and wN,#bit; cbz/cbnz` + that fold to a single `tbz/tbnz`; clang uses tbz/tbnz 3,102 times, kit 0. The + *narrow* single-bit-AND-feeding-an-adjacent-branch case is arguably LINEAR (a + `pass_native_emit`/`pass_jump` peephole dropping the `and` and emitting tbz/tbnz); + it is listed here rather than §2 because the broader csel/ccmp family it sits inside + is SSA-shaped, and the narrow slice should be validated before committing. → mostly + O2, with a possible linear `tbz` slice worth a spike. +- **Cross-block redundant re-extension / re-materialization.** Repeated `sxtw rX,wY` + of an unchanged source across blocks/calls (sqlite ~173 conservatively measured; + more in reality). L6/L8 handle the same-block and addressing-mode-foldable cases; + the cross-block redundant *re*-extend needs GVN / cross-block copy propagation. → O2 GVN. +- **FP constant folded at the wrong (narrower) width.** tinyexpr's `NAN = 0.0/0.0` / + `INFINITY = 1.0/0.0` fold to a *float* NaN/Inf, materialize into a GPR, then `fcvt + d,s` to widen (12 sites, +24 insns). Folding at the *result* width avoids the + widen, but correctness needs care that float→double of the folded constant is the + same value (canonical NaN payload), so it is a width-selection fix in the + constant-fold path, not a trivial peephole. Marked NLOGN/needs-care. → folder fix + (cg_ir_lower.c), schedule with care, low priority. + +--- + +## 4. Not a deficiency + +Where clang is bigger via inlining/unrolling, or kit is already smaller — so future +readers don't chase these. + +- **clang inlines/unrolls far more at `-O1`.** lz4 (kit **0.38×** clang) and yyjson + (0.86×), lapi (0.83×) are *smaller* in kit because clang inlines aggressively + (yyjson kit bl=942 vs clang 294; lz4 `LZ4_compress_generic`/`LZ4_count`/`LZ4_hash5` + fully inlined into every caller). Every clang-heavy mnemonic on those files + (movz/cmp/add/sub/movk/csel/madd/strh/ldrh) is an inline-site materialization/ + selection artifact, **not a kit gap**. Tuning kit's inline-pressure cap (W4, landed) + *upward* would increase total `__TEXT`, not decrease it — net size impact + neutral-to-negative. +- **`ret` / function-count deltas are inlining shape.** lparser kit 96 funcs / 96 ret + vs clang 24 / 20; kit emits exactly one ret per function (optimal per-function, no + duplicated epilogues). tinyexpr `blr` 16 vs clang `br` (tail-call dispatch after + inlining). These belong to the parked O2 inliner/tail-call work. +- **NEON / `.inst` columns.** clang's hundreds of `.inst` are `umulh`/`smulh` + magic-multiply divides, `movi` vector zeroing, and `stp q` — vectorization/strength- + reduction kit doesn't do. The zero-init half is recoverable as **L3** (scalar + widening); the magic-multiply half is the optional L9 strength-reduction tail. +- **`cbz` deltas favoring clang.** cjson `cbz` is −107 (kit 208 vs clang 315) — kit is + *not* over-emitting branches here; do not read the raw cbz delta as a kit gap. + +**Downgraded on spot-check:** the lz4/miniz "address bases spilled and reloaded" and +"large struct-field address rebuilt per use" items the per-file agents rated high +were largely the *cross-block* residency problem (NEEDS-SSA-O2, §3), not a clean +linear peephole — only their strictly-intra-block, same-base-unchanged subset is +linear, and that subset is already covered by **L1** (store→load forwarding) and the +landed **W1a** (frame-address sub/add-CSE) + **W5** (local CSE). They are not given +separate §2 entries to avoid double-counting. + +--- + +## 5. Suggested next O1 work + +In the style of O1.md's worklist — ranked by impact × confidence × breadth, each a +linear no-SSA change with a clear host pass and a `test/opt/` structural guard. + +1. **P1 — store→load forwarding on register mismatch** (`pass_combine.c`, + `opt_combine_compact_block`). Highest single lever (lvm ~1400 insns, plus lz4/ + sqlite/miniz intra-block). Drop the `same_reg_operand` requirement in the + store;load branch; emit a copy on mismatch, let substitution + W8 + mir_dce clean + up. Guard: assert the str;ldr-different-reg pattern is gone on an lvm fixture. +2. **P2 — `cset;cbnz/cbz` → `b.cc` fusion** (`pass_combine.c`/`pass_jump.c`). Broadest + cross-file win (yyjson 438, lapi 27, lvm/lz4/sqlite). Fuse `IR_CONDBR` of a + single-use `IR_CMP` into `IR_CMP_BRANCH`; inverse of the existing cg_ir_lower fusion. +3. **P3 — widen `aa_set_bytes` zero-init** (`aa64/native.c`). ~25% of cjson's gap; + helps miniz. Emit `str xzr`/`stp q0` aligned runs (or `bl _memset`) instead of + per-byte `strb`. Self-contained expander change. +4. **P4 — drop yyjson double-cset + redundant `sxtb`-after-`ldrsb`** (`pass_combine.c` + / `combine_exts`). ~1100 + ~186 insns on yyjson. Same-block last-def peepholes; + the sxtb-drop is the signed mirror of the landed ZEXT-of-load fold. +5. **P5 — constant-operand div/mul fold + same-block `LOAD_IMM` const tracker** + (`pass_simplify.c`). Fixes lparser being larger than clang (the SIZE_MAX guard) + + sqlite's 108 const-divide sites. A bounded per-block reg→value map feeding the + existing BO_UDIV/SDIV folds. +6. **P6 — single-use copy coalescing into convert/store/call-arg/return consumers** + (`pass_combine.c`) and **`sxtw`-index-into-addressing-mode** (`pass_addr_fold.c`). + The `mov` and standalone-`sxtw` surplus across the value-heavy files; both reuse + existing CombineCtx/addr-fold machinery. + +**Definition of done per item:** measurable `__TEXT`/instruction reduction on the +affected corpus files (via `scripts/o1_quality.sh`), sqlite `-O1` compile time +unchanged (linearity held — the §8 synthetic sweep), all correctness gates green +(`make test-opt test-toy`, ecosystem `-O0`/`-O1` vs clang, smoke-x64/rv64), and a +`test/opt/` structural disasm guard added. + +> Sequencing note: P1 + P3 are the two biggest size levers and touch disjoint files +> (`pass_combine.c` vs `aa64/native.c`) → parallelizable. P2/P4/P5/P6 share +> `pass_combine.c`/`pass_simplify.c` → one owner, land + re-measure one at a time +> (gate = correctness, not byte-identity). The true ceiling on lvm/sqlite/miniz/lz4 +> remains the SSA register allocator (§3) — these linear items close the *local* +> waste, not the over-spilling. diff --git a/doc/plan/O1.md b/doc/plan/O1.md @@ -71,7 +71,10 @@ diff text/mix across the corpus) — it was ad-hoc for this analysis. > sqlite 35,360 → 0; lvm `__TEXT` 96,788 → 59,276, **4.66× → 2.86×**; sqlite > 1.27× → 1.11×; miniz 1.94× → 1.67×; the clang-favored lz4/yyjson held at > 0.38×/0.86×). sqlite `-O1` compile stayed ≈1.6→1.67s (linear; W3 the main -> contributor). The pre-campaign analysis below is retained for context. +> contributor). The pre-campaign analysis below is retained for context. **The +> residual gap is now catalogued in [O1-PATTERNS.md](O1-PATTERNS.md)** — a +> kit-vs-clang disassembly audit of what remains, ranking the further linear/ +> no-SSA wins (L1–L10) and separating them from the parked-O2 SSA gaps. **[Pre-campaign] Aggregate `__TEXT` ≈ 1.25× clang `-O1`** across the corpus — but the ratio is bimodal and the aggregate understates the truth: