commit f75874cb64e948bc45a02ff2eb90bdb4b5457da3
parent 3516b3d341e45839a621d05bf189000302a461f0
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 12 Jun 2026 19:27:35 -0700
perf(cg): route -O0 address results through the write-back cache
The single-pass address producers (nd_addr_of, nd_local_addr, nd_load_label_addr)
computed the address into a scratch register and then stored it to the result
local's home (nd_dst_scratch + nd_store_operand_from_reg). When the address feeds
an immediate consumer — overwhelmingly the load it was computed for, or the base
of one — that meant a spill + reload around every &member / &local / &arr[i].
Route them through the write-back register cache exactly like nd_load already
does (nd_dst_reg + nd_dst_writeback): load_addr writes straight into the dst
local's cache register, which the consumer then reads directly. The address
temps are still pinned and nd_cache_alloc skips pinned regs, so the cache
register never aliases the base/index. Caching a computed address is
correctness-equivalent to caching a loaded pointer (which nd_load already does)
and is dropped at every barrier.
sqlite3.c -c, arm64-macOS: 599,488 -> 586,773 insns (-2.1%); object 2.79 -> 2.74MB.
Combined with the temp-slot reuse this run: 831,078 -> 586,773 (-29.4%), now
~1.71x tcc. (The remaining ~63K reg-reg movs are kit_cg_copy value-stack copies
between distinct locals; eliminating those needs liveness-based storage
coalescing, deliberately out of scope here.)
test-toy 1392/0, test-parse-ok 3920/0 (O0/O1 x D/R/J/E), test-parse-err 129/0,
test-smoke-x64/rv64 3/0; sqlite shell links+runs (84|2).
Diffstat:
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c
@@ -1064,11 +1064,13 @@ static void nd_local_addr(CgTarget* t, Operand dst, const CGLocalDesc* desc,
lv.type = l->type;
lv.v.local = local;
{
- NativeLoc reg = nd_dst_scratch(d, dst);
+ /* Route the &local address through the write-back cache (see nd_addr_of):
+ * write it into dst's cache register rather than a scratch copied to the
+ * home, killing the mov and keeping it resident for an immediate consumer. */
+ NativeLoc reg = nd_dst_reg(d, dst);
ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
d->native->load_addr(d->native, reg, nd_addr_storage(d, lv));
- nd_store_operand_from_reg(d, dst, reg);
- nd_release_materialized(d, reg);
+ nd_dst_writeback(d, dst, reg);
}
}
@@ -1164,12 +1166,11 @@ static void nd_load_label_addr(CgTarget* t, Operand dst, Label label) {
NativeDirectTarget* d = nd_of(t);
NativeLoc reg;
nd_flush_all(d);
- reg = nd_dst_scratch(d, dst);
+ reg = nd_dst_reg(d, dst);
ND_REQUIRE_NATIVE(d, load_label_addr,
"target does not materialize label addresses");
d->native->load_label_addr(d->native, reg, nd_mc_label(d, label));
- nd_store_operand_from_reg(d, dst, reg);
- nd_release_materialized(d, reg);
+ nd_dst_writeback(d, dst, reg);
}
static int nd_local_static_data_begin(CgTarget* t,
@@ -1484,11 +1485,17 @@ static void nd_addr_of(CgTarget* t, Operand dst, Operand lv) {
l->flags |= CG_LOCAL_ADDR_TAKEN;
}
naddr = nd_addr_materialize(d, nd_addr_storage(d, lv), &temps, mem);
- NativeLoc reg = nd_dst_scratch(d, dst);
+ /* Route the address through the write-back register cache (like nd_load):
+ * load_addr writes straight into the dst local's cache register instead of a
+ * scratch that is then copied to the home, killing the mov and keeping the
+ * address resident for an immediate consumer (e.g. the base of the load it
+ * feeds). nd_dst_reg falls back to a scratch + home store via nd_dst_writeback
+ * when dst is not cacheable. The address temps are still pinned, and
+ * nd_cache_alloc skips pinned regs, so the cache reg never aliases base/index. */
+ NativeLoc reg = nd_dst_reg(d, dst);
ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
d->native->load_addr(d->native, reg, naddr);
- nd_store_operand_from_reg(d, dst, reg);
- nd_release_materialized(d, reg);
+ nd_dst_writeback(d, dst, reg);
nd_addr_temps_release(d, &temps);
}