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:
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