kit

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

commit a35a48341c3cf53067fb777e0dd7ad3b36e4b92e
parent 06bbc026b294d80cc9ed27fba745d9ed3220d922
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 08:28:10 -0700

opt: fuse cmp;cset;cbnz/cbz into cmp_branch at O1 (O1-PATTERNS L2)

A relational whose 0/1 bool was materialized into a register and then
re-tested by the terminator lowers, at MIR, to IR_CMP rD followed by the
terminator IR_CMP_BRANCH(CMP_NE|CMP_EQ, rD, #0). When rD's most-recent
same-block def is a single-use IR_CMP, try_fuse_cmp_branch rewrites the
branch to test the cmp's own relation (inverting for the cbz/EQ-vs-0 case
via the full CmpOp invert table, correct for FP NaN too) and NOPs the
cmp. The inverse of the SSA fusion in pass_o2.c ssa_combine_fold_cmp_branch,
here over the no-SSA CombineCtx (last-def + hard-live single-use). No
condition inversion is needed for cbnz; cbz inverts the cmp op.

SAME-BLOCK SCOPE: at no-SSA O1 many s split the cmp and its branch
into different MIR blocks (a scope_begin/block boundary the prepare-time
jump cleanup does not concatenate), which a same-block peephole cannot
reach. L2 fuses the co-located subset (inlined predicates etc.): yyjson.c
-O1 cset 1780 -> 1704, lvm cset;cbr 19->15, lapi 27->13. The broad
cross-block residual the catalog counted from post-emit disasm adjacency
needs the SSA path / post-combine block merging and is out of scope here.

Inversion verified correct across every relation x both branch polarities
x FP NaN/inf (kit O0==O1==clang). Battery + cjson/yyjson/sqlite/lua -O1
run-correct; x64/rv64/aa64 cross-compile clean.

Adds the L2 red-green section to test/opt/o1p_combine.sh (yyjson cset
count + all-relations/both-polarities/FP O0==O1 inversion check).

Diffstat:
Msrc/opt/pass_combine.c | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/opt/o1p_combine.sh | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 187 insertions(+), 0 deletions(-)

diff --git a/src/opt/pass_combine.c b/src/opt/pass_combine.c @@ -1405,6 +1405,128 @@ static int try_combine_exts(CombineCtx* ctx, Inst* in, i32 i) { return 0; } +/* Invert a CmpOp (negate the relation). Returns 0 for an op outside the + * known total set. Kept byte-for-byte in sync with src/cg/fold.c + * api_invert_cmp and pass_jump.c invert_cmp — including the FP rule that + * negation flips ordered<->unordered (so the NaN outcome flips too): the + * negation of ordered `a<b` is *unordered* `a>=b`. */ +static int combine_invert_cmp(CmpOp op, CmpOp* out) { + switch (op) { + case CMP_EQ: *out = CMP_NE; return 1; + case CMP_NE: *out = CMP_EQ; return 1; + case CMP_LT_S: *out = CMP_GE_S; return 1; + case CMP_LE_S: *out = CMP_GT_S; return 1; + case CMP_GT_S: *out = CMP_LE_S; return 1; + case CMP_GE_S: *out = CMP_LT_S; return 1; + case CMP_LT_U: *out = CMP_GE_U; return 1; + case CMP_LE_U: *out = CMP_GT_U; return 1; + case CMP_GT_U: *out = CMP_LE_U; return 1; + case CMP_GE_U: *out = CMP_LT_U; return 1; + case CMP_OEQ_F: *out = CMP_UNE_F; return 1; + case CMP_ONE_F: *out = CMP_UEQ_F; return 1; + case CMP_OLT_F: *out = CMP_UGE_F; return 1; + case CMP_OLE_F: *out = CMP_UGT_F; return 1; + case CMP_OGT_F: *out = CMP_ULE_F; return 1; + case CMP_OGE_F: *out = CMP_ULT_F; return 1; + case CMP_UEQ_F: *out = CMP_ONE_F; return 1; + case CMP_UNE_F: *out = CMP_OEQ_F; return 1; + case CMP_ULT_F: *out = CMP_OGE_F; return 1; + case CMP_ULE_F: *out = CMP_OGT_F; return 1; + case CMP_UGT_F: *out = CMP_OLE_F; return 1; + case CMP_UGE_F: *out = CMP_OLT_F; return 1; + } + return 0; +} + +/* ---- L2 (O1-PATTERNS): fuse cmp rD; cmp_branch(NE/EQ, rD, #0) -> cmp_branch + * + * A relational whose 0/1 bool was MATERIALIZED into a register (`cmp; cset + * rD,cc`) and then re-tested by the branch (`cbnz/cbz rD`) lowers, at this + * MIR level, to `IR_CMP rD = (a cc b)` followed by the terminator + * `IR_CMP_BRANCH(CMP_NE|CMP_EQ, rD, #0)` (control.c synthesizes cmp_branch of + * the bool against IMM_ZERO; cg's delayed-compare path already fuses the + * un-materialized `if (a<b)` form upstream, so only the materialized residual + * reaches here). The branch's NE/EQ-vs-0 just re-tests a bool the original cmp + * already computed in flags. + * + * When the terminator is `IR_CMP_BRANCH(NE|EQ, rD, #0)` whose rD's most-recent + * same-block def is a single-use IR_CMP, fuse into the cmp's own relation: + * cbnz (CMP_NE vs 0): branch-if-true -> cmp_branch(cmp.op, a, b) + * cbz (CMP_EQ vs 0): branch-if-false -> cmp_branch(invert(cmp.op), a, b) + * and NOP the now-dead cmp. This is the inverse of the SSA fusion in + * pass_o2.c (ssa_combine_fold_cmp_branch), here over the no-SSA CombineCtx + * (last-def map + hard-live single-use). Inversion uses the full CmpOp table + * (combine_invert_cmp), which is correct for FP too: the cset collapsed the + * comparison to a definite 0/1 even for NaN, and the inverse op reproduces the + * same NaN branch direction (e.g. !(a OEQ b) == a UNE b). + * + * Single-block, single forward pass, all guards from existing CombineCtx state + * -> linear. */ +static int try_fuse_cmp_branch(CombineCtx* ctx, Inst* in, i32 i) { + if ((IROp)in->op != IR_CMP_BRANCH || in->nopnds < 2) return 0; + /* Only the block terminator carries the two CFG successors. */ + if (i != (i32)ctx->bl->ninsts - 1 || ctx->bl->nsucc < 2) return 0; + /* Branch must test a register bool against immediate 0 with EQ/NE. */ + CmpOp brop = (CmpOp)in->extra.imm; + if (brop != CMP_EQ && brop != CMP_NE) return 0; + if (in->opnds[0].kind != OPK_REG) return 0; + if (in->opnds[1].kind != OPK_IMM || in->opnds[1].v.imm != 0) return 0; + + Operand cond = in->opnds[0]; + i32 prod_idx = ctx_producer_of(ctx, cond.cls, cond.v.reg); + if (prod_idx < 0 || prod_idx >= i) return 0; + Inst* cmp = &ctx->bl->insts[prod_idx]; + if ((IROp)cmp->op != IR_CMP || cmp->nopnds < 3) return 0; + if (cmp->opnds[0].kind != OPK_REG || !same_phys_reg(&cmp->opnds[0], &cond)) + return 0; + + /* Resolve the fused relation (invert for the cbz / EQ-vs-0 case). */ + CmpOp fused = (CmpOp)cmp->extra.imm; + if (brop == CMP_EQ && !combine_invert_cmp(fused, &fused)) return 0; + + /* The cmp's input operands must be unchanged between the cmp and the branch + * (an intervening inst may have redefined a register the cmp reads). */ + for (u32 oi = 1; oi < 3; ++oi) { + const Operand* p = &cmp->opnds[oi]; + if (p->kind == OPK_REG && + ctx_def_changed_since(ctx, p->cls, p->v.reg, prod_idx)) + return 0; + } + + /* The cmp's result must die at this branch: its sole use, not live-out. If it + * has other uses (or escapes the block) the cmp must stay and we cannot NOP + * it. */ + Operand cmp_def = cmp->opnds[0]; + int killed = 0; + if (count_uses_in_live_range(ctx->f, ctx->bl, prod_idx, &cmp_def, &killed) != + 1) + return 0; + if (!killed && opt_block_live_out_has_phys_reg(ctx->f, ctx->hard_live, + ctx->bl->id, &cmp_def)) + return 0; + + /* Fuse: rewrite the branch to test the cmp's own relation directly; NOP the + * cmp. Successors are unchanged (succ[0]=taken, succ[1]=fallthrough). */ + Operand* opnds = arena_array(ctx->f->arena, Operand, 2); + opnds[0] = cmp->opnds[1]; + opnds[1] = cmp->opnds[2]; + in->opnds = opnds; + in->nopnds = 2; + in->extra.imm = (i64)fused; + + cmp->op = (u16)IR_NOP; + cmp->def = VAL_NONE; + cmp->ndefs = 0; + cmp->defs = NULL; + cmp->nopnds = 0; + cmp->opnds = NULL; + /* The cmp no longer defines cond's register; restore its prior reaching def + * so later same-block availability checks stay accurate. */ + ctx_restore_removed_def(ctx, &cond, prod_idx); + ctx->block_change_p = 1; + return 1; +} + /* ---- Rewrite 6 (W1a): local frame-address `sub`-CSE ---- * * O1.md W1a. An `IR_ADDR_OF rD, <addr>` materializes a frame-slot or global @@ -1917,6 +2039,10 @@ static int opt_combine_fold_block(Func* f, Block* bl, try_addr_of_cse(&ctx, in, i); try_fold_const_convert(&ctx, in, i); try_combine_exts(&ctx, in, i); + /* L2: fuse a same-block cmp feeding the IR_CONDBR terminator into an + * IR_CMP_BRANCH (run before try_cmp_imm_fold so the cmp's immediate slot + * still folds on the fused branch). */ + try_fuse_cmp_branch(&ctx, in, i); try_substitute(&ctx, in, i); try_addr_synth(&ctx, in, i); /* W5: same-block redundant-load + pure-compute reuse. Run after diff --git a/test/opt/o1p_combine.sh b/test/opt/o1p_combine.sh @@ -195,4 +195,65 @@ EOF [ "$L6_O0" = "$L6_O1" ] || fail "L6 run O0/O1 differ: $L6_O0 vs $L6_O1" printf ' L6: run rc=%s (O0==O1)\n' "$L6_O0" +# ============================================================================ +# L2 — fuse cmp rD; cmp_branch(NE/EQ, rD, #0) -> cmp_branch (boolean-into-branch) +# ============================================================================ +# When a relational's 0/1 bool was materialized into a register (`cset rD`) and +# then re-tested by the terminator (`cbnz/cbz rD`), and the cmp + branch land in +# the SAME MIR block (single-use cmp), try_fuse_cmp_branch rewrites the branch to +# test the cmp's own relation and NOPs the cmp. (Many `if`s split the cmp and +# branch into different blocks at no-SSA O1 — those are out of reach for a +# same-block peephole; L2 captures the co-located subset, e.g. inlined +# predicates in yyjson.) Structural signal: fewer `cset` on yyjson. +echo "== L2 cmp;cset;cbnz/cbz -> cmp_branch fusion ==" +if [ -n "${YY_SRC:-}" ] && [ -f "$YY_SRC/yyjson.c" ] && [ -n "$SYS" ]; then + # candidate yy.cand.dis was produced by the L6 section above. + CAND_CSET="$(grep -cE '\bcset\b' "$WORK/yy.cand.dis" || true)" + if [ "$have_base" = 1 ]; then + BASE_CSET="$(grep -cE '\bcset\b' "$WORK/yy.base.dis" || true)" + [ "$BASE_CSET" -ge 100 ] || fail "L2 red precondition: expected baseline yyjson many cset (got $BASE_CSET)" + [ "$CAND_CSET" -lt "$BASE_CSET" ] || fail "L2: candidate did not reduce cset (base=$BASE_CSET cand=$CAND_CSET)" + printf ' L2: yyjson cset base=%s -> cand=%s (RED->GREEN; L1/L6/L2 cumulative)\n' "$BASE_CSET" "$CAND_CSET" + else + printf ' L2: yyjson cset cand=%s (green-only)\n' "$CAND_CSET" + fi +else + printf ' L2: SKIP structural (yyjson.c not provisioned or no SDK)\n' +fi + +# L2 correctness — the critical trap is mis-inverting the condition. Exercise +# every relation in BOTH branch polarities (if(t) and if(!t)) plus FP NaN/inf, +# forcing materialization, and require kit -O0 == kit -O1. +cat > "$WORK/l2run.c" <<'EOF' +static int hits = 0; +static void use(int x) { hits += x; } +static long f(int a, int b, double x, double y) { + int lt=(a<b); if(lt)use(1); if(!lt)use(2); + int le=(a<=b); if(le)use(4); if(!le)use(8); + int gt=(a>b); if(gt)use(16); if(!gt)use(32); + int ge=(a>=b); if(ge)use(64); if(!ge)use(128); + int eq=(a==b); if(eq)use(256); if(!eq)use(512); + int ne=(a!=b); if(ne)use(1024); if(!ne)use(2048); + unsigned ua=(unsigned)a, ub=(unsigned)b; + int ult=(ua<ub); if(ult)use(4096); if(!ult)use(8192); + int uge=(ua>=ub); if(uge)use(16384); if(!uge)use(32768); + int feq=(x==y); if(feq)use(1<<16); if(!feq)use(1<<17); + int flt=(x<y); if(flt)use(1<<18); if(!flt)use(1<<19); + return hits; +} +int main(void) { + long acc = 0; + double ds[5] = {-1.0, 0.0, 1.0, 2.0, 3.0/(double)(0.0==1.0 ? 1.0 : 1e308*1e308)}; + for (int a=-3;a<=3;a++) for (int b=-3;b<=3;b++) + for (int i=0;i<5;i++) for (int j=0;j<5;j++) { hits=0; acc=acc*131+f(a,b,ds[i],ds[j]); } + return (int)(acc & 0x7f); +} +EOF +"$KIT" cc -O0 -std=c11 "$WORK/l2run.c" -o "$WORK/l2run.o0" >"$WORK/l2run.o0.cc" 2>&1 || fail "L2 run -O0 compile" "$WORK/l2run.o0.cc" +"$KIT" cc -O1 -std=c11 "$WORK/l2run.c" -o "$WORK/l2run.o1" >"$WORK/l2run.o1.cc" 2>&1 || fail "L2 run -O1 compile" "$WORK/l2run.o1.cc" +"$WORK/l2run.o0"; L2_O0=$? +"$WORK/l2run.o1"; L2_O1=$? +[ "$L2_O0" = "$L2_O1" ] || fail "L2 run O0/O1 differ (mis-inverted condition?): $L2_O0 vs $L2_O1" +printf ' L2: run rc=%s (O0==O1; all relations x both polarities x FP)\n' "$L2_O0" + echo "o1p_combine: OK"