kit

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

commit 0cac2e5ced0729fe4cfe3359319142e8358a615e
parent 99899453fcf7fad2a7ba0eed9e341c78298e86cf
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 16:50:32 -0700

perf(cg): -O0 switch-selector residency + indirect-branch reorder (§4.6)

Diffstat:
Msrc/cg/native_direct_target.c | 55++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 52 insertions(+), 3 deletions(-)

diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -1334,9 +1334,52 @@ static void nd_cmp_branch(CgTarget* t, CmpOp op, Operand a, Operand b, nd_release_materialized(d, ar); } +/* Cmp-and-branch chain with the selector pinned in ONE register across every + * case. This emits the same shape as cg_lower_switch_default (one CMP_EQ per + * case against cases[i].value at selector_type, then a jump to the default if + * any), but materializes the selector ONCE — before the flush — instead of + * letting each per-case cmp_branch reload it from its home (the flush empties + * the cache, so the shared lowering would re-materialize + re-flush per case). + * + * Soundness: after the initial flush the cache is empty. The chain falls through + * case-to-case with that same empty state — sel is a pinned scratch reg, not a + * cache entry, so nothing is re-cached between cases. Each case body and the + * default are reached through nd_label_place / nd_jump, which flush an + * already-empty cache. So the cache is empty along every out-edge: merge-correct + * exactly as the shared lowering is. The selector's value survives the flush in + * its pinned register (a spill store does not clobber it). Other backends and + * opt's IR replay still use cg_lower_switch_default unchanged. */ static void nd_switch(CgTarget* t, const CGSwitchDesc* desc) { - nd_flush_all(nd_of(t)); - cg_lower_switch_default(t, desc); + NativeDirectTarget* d = nd_of(t); + NativeLoc sel; + if (desc->ncases == 0) { + nd_flush_all(d); + if (desc->default_label != LABEL_NONE) { + ND_REQUIRE_NATIVE(d, jump, "target does not emit jumps"); + d->native->jump(d->native, nd_mc_label(d, desc->default_label)); + } + return; + } + sel = nd_materialize_operand(d, desc->selector); + nd_flush_all(d); + ND_REQUIRE_NATIVE(d, cmp_branch, "target does not emit compare branches"); + for (u32 i = 0; i < desc->ncases; ++i) { + /* Same per-case immediate cg_lower_switch_default builds with api_op_imm: + * cases[i].value interpreted at selector_type. Constructed inline (the imm + * helper is not declared in this TU). */ + Operand imm = {.kind = OPK_IMM, + .type = desc->selector_type, + .v.imm = (i64)desc->cases[i].value}; + NativeLoc br = nd_rhs_imm_or_reg(d, NATIVE_IMM_CMP, (u32)CMP_EQ, imm); + d->native->cmp_branch(d->native, CMP_EQ, sel, br, + nd_mc_label(d, desc->cases[i].label)); + nd_release_materialized(d, br); + } + if (desc->default_label != LABEL_NONE) { + ND_REQUIRE_NATIVE(d, jump, "target does not emit jumps"); + d->native->jump(d->native, nd_mc_label(d, desc->default_label)); + } + nd_release_materialized(d, sel); } static void nd_indirect_branch(CgTarget* t, Operand addr, @@ -1344,8 +1387,14 @@ static void nd_indirect_branch(CgTarget* t, Operand addr, NativeDirectTarget* d = nd_of(t); MCLabel* native_targets; NativeLoc addr_reg; - nd_flush_all(d); + /* Materialize the target address BEFORE the flush (mirrors nd_cmp_branch): a + * cache-resident address is read from its register instead of being spilled + * by the flush and immediately reloaded. The reg stays pinned across the + * flush — its value survives (a spill store does not clobber it) — and the + * post-branch cache is still empty, so merge correctness is unchanged. */ addr_reg = nd_materialize_operand(d, addr); + nd_drop_killed_operand(d, addr); + nd_flush_all(d); ND_REQUIRE_NATIVE(d, indirect_branch, "target does not emit indirect branches"); native_targets =