kit

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

commit ab2c6c1f7ad364ce10f2e66f899745e76d4a29b8
parent b4b5067ac11a2340ed56164c3e0fa60ec5a55651
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 13:31:11 -0700

perf(cg): -O0 elide the zero-extend after a narrow integer load (L3, unsigned)

A narrow integer load (ldrb/ldrh/ldr-w on aa64, movzx on x64, lbu/lhu/lwu on
rv64) zero-extends the whole destination register, so a CV_ZEXT widening that
value is a no-op. Record a load_zext flag on the loaded local (set in nd_load,
cleared on any other write / on flush / on invalidate — valid only while the
register holds the load result) and, when nd_convert coalesces a CV_ZEXT whose
source carries it, skip the extend entirely (the upper bits are provably clear).
Arch-neutral: every supported backend's generic narrow load zero-extends.

The signed case (ldrsb/ldrsh) is not handled — kit's CG integer types are
sign-agnostic, so the load cannot know it feeds a sign-extend; that needs a
frontend widening-load and is left as a follow-up.

sqlite3.c -O0 .text: -3,587 insns (uxtb -2,227, uxth -1,107, ubfx -253);
1.1730x -> 1.1625x tcc. Gate: determinism + sqlite e2e O0/O1 (golden+vs-clang)
+ toy/parse/smoke-x64/smoke-rv64/dwarf/debug all green; alloca + far-slot +
narrow-load + p1-x64 clang-differential probes match clang.

Diffstat:
Msrc/cg/native_direct_target.c | 38++++++++++++++++++++++++++++++++------
Msrc/cg/native_direct_target.h | 7+++++++
2 files changed, 39 insertions(+), 6 deletions(-)

diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -703,6 +703,7 @@ static void nd_flush_local(NativeDirectTarget* d, CGLocal local) { d->reg_owner[l->cls][l->reg] = CG_LOCAL_NONE; l->reg = REG_NONE; l->dirty = 0; + l->load_zext = 0; } /* Drop a cache entry without writing it back, for when a store supersedes the @@ -714,6 +715,7 @@ static void nd_invalidate_local(NativeDirectTarget* d, CGLocal local) { d->reg_owner[l->cls][l->reg] = CG_LOCAL_NONE; l->reg = REG_NONE; l->dirty = 0; + l->load_zext = 0; } /* Spill the whole cache to memory and empty it. The list is sorted ascending, @@ -1043,6 +1045,7 @@ static void nd_dst_writeback(NativeDirectTarget* d, Operand dst, NativeLoc dr) { if (dr.kind == NATIVE_LOC_REG && l->reg == dr.v.reg && dst.type == l->type && nd_local_cacheable(d, l)) { l->dirty = 1; + l->load_zext = 0; /* a fresh value; nd_load re-sets this for narrow loads */ d->scratch_used[l->cls] &= ~(1u << dr.v.reg); /* unpin, keep cached */ return; } @@ -1600,6 +1603,16 @@ static void nd_load(CgTarget* t, Operand dst, Operand addr, MemAccess mem) { ND_REQUIRE_NATIVE(d, load, "target does not emit loads"); d->native->load(d->native, reg, naddr, mem); nd_dst_writeback(d, dst, reg); + /* A narrow integer load zero-extends the whole register (ldrb/ldrh/ldr-w + * clear the upper bits), so a subsequent CV_ZEXT that widens this value is a + * no-op. Record it when the result stayed register-resident so nd_convert can + * drop the extend. (nd_dst_writeback just cleared the flag for this local.) */ + if (dst.kind == OPK_LOCAL && size < (u64)t->c->target.ptr_size && + nd_class_for_type(d, dst.type) == NATIVE_REG_INT && + reg.kind == NATIVE_LOC_REG) { + NativeDirectLocal* l = nd_local(d, dst.v.local); + if (l->reg == reg.v.reg) l->load_zext = 1; + } nd_addr_temps_release(d, &temps); } @@ -1897,18 +1910,31 @@ static void nd_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) { * collapse to nothing in the arch move/convert path. A class-crossing convert * (GPR<->FPR fmov) is excluded by nd_rename_killed_to_dst's same-class guard * and falls through below. */ + /* A CV_ZEXT whose source came straight from a narrow integer load is a no-op: + * the load already zero-extended the whole register (load_zext). Detect it + * before the rename, which transfers (and would clear) the source's flag. */ + int zext_noop = 0; + if (op == CV_ZEXT && src.kind == OPK_LOCAL) { + NativeDirectLocal* sl = nd_local(d, src.v.local); + zext_noop = sl->reg != REG_NONE && sl->load_zext && src.type == sl->type; + } { Reg r = nd_rename_killed_to_dst(d, dst, src); if (r != REG_NONE) { NativeAllocClass cls = nd_class_for_type(d, dst.type); sr = nd_loc_reg(d, src.type, cls, r); dr = nd_loc_reg(d, dst.type, cls, r); - if (d->native->convert_rr) - d->native->convert_rr(d->native, op, native_reg_loc_of(dr), - native_reg_loc_of(sr)); - else { - ND_REQUIRE_NATIVE(d, convert, "target does not emit converts"); - d->native->convert(d->native, op, dr, sr); + /* Renaming the source register to the destination already placed the + * zero-extended value where the result wants it — skip the redundant + * extend; the upper bits are provably clear. */ + if (!zext_noop) { + if (d->native->convert_rr) + d->native->convert_rr(d->native, op, native_reg_loc_of(dr), + native_reg_loc_of(sr)); + else { + ND_REQUIRE_NATIVE(d, convert, "target does not emit converts"); + d->native->convert(d->native, op, dr, sr); + } } nd_dst_writeback(d, dst, dr); return; diff --git a/src/cg/native_direct_target.h b/src/cg/native_direct_target.h @@ -41,6 +41,13 @@ typedef struct NativeDirectLocal { u8 memory_required; u8 transient; /* a per-subexpression temp; its home is reclaimable at a * statement boundary (see nd_reclaim_temps) */ + /* The cached register holds this local's value zero-extended to the full + * register, straight from a narrow integer load (ldrb/ldrh/ldr-w always zero + * the upper bits). While set, a CV_ZEXT widening it is a no-op and is elided + * by nd_convert. Set only in nd_load; cleared on any other write + * (nd_dst_writeback) and when the register is dropped (flush/invalidate). + * Meaningful only while reg != REG_NONE. */ + u8 load_zext; u32 last_use; /* d->use_tick at the most recent cache touch (LRU victim key) */ /* Intrusive doubly-linked list of currently-cached locals, in insertion