commit 93aafda346d271d17ce2ea95abb2083e4ff896b2
parent 94a15adbf27f18e030686e70efaa88605dc9d9bd
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 06:31:47 -0700
perf(cg): -O0 register-resident return — drop the ret home round-trip
nd_ret flushed the whole value cache and then plan_ret read the return
value back from its frame home, emitting `stur <result>,[home]; ldur
x0,[home]` (plus dead writebacks) in every function. At a return every
cached local is dead — the frame is about to be torn down — so source the
return value straight from its live register when cached and discard the
cache without writing it back (nd_drop_all). A non-cached value (multi-part
/ aggregate / memory-required) stays authoritative in its home, and the
emit_ret ops path (no production arch installs it) keeps the legacy
flush-then-read behavior.
Return path: `stur w8,[home]; ldur w0,[home]` -> `mov w0, w8`.
sqlite3.c -c -O0 (arm64-macOS): emitted insns 526,681 -> 520,854 (-5,827);
object 2,501,224 -> 2,477,912 B (-0.93%); compile instrs 2,153.1M ->
2,150.9M. Byte-deterministic (compile x2 identical).
Green: test-toy 1392/0, test-parse-ok 3920/0, test-parse-err 129/0,
test-smoke-x64 3/0, test-smoke-rv64 3/0; sqlite end-to-end 84|2.
Diffstat:
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c
@@ -666,6 +666,16 @@ static void nd_flush_all(NativeDirectTarget* d) {
while (d->cache_head >= 0) nd_flush_local(d, (CGLocal)(d->cache_head + 1));
}
+/* Discard the whole cache WITHOUT writing entries back. Only sound where every
+ * cached value is provably dead — at a return the frame is about to be
+ * destroyed, so no home is ever read again. Lets nd_ret source the return value
+ * from its live register and skip the round-trip + the dead writebacks a
+ * flush-all would emit. */
+static void nd_drop_all(NativeDirectTarget* d) {
+ while (d->cache_head >= 0)
+ nd_invalidate_local(d, (CGLocal)(d->cache_head + 1));
+}
+
static NativeAddr nd_addr_materialize(NativeDirectTarget* d, NativeAddr in,
NdAddrTemps* temps, MemAccess mem) {
NativeAddr out = in;
@@ -1776,15 +1786,27 @@ static void nd_ret(CgTarget* t, CGLocal value) {
const NativeLoc* locp = NULL;
NativeCallPlanRet* rets = NULL;
u32 nrets = 0;
- nd_flush_all(d);
+ /* The emit_ret ops path reads `value` from its home, so it needs the cache
+ * spilled first (no production arch installs it today). */
if (d->ops && d->ops->emit_ret) {
+ nd_flush_all(d);
d->ops->emit_ret(d, value);
return;
}
+ /* Default path: at a return every cached value is dead (the frame is about to
+ * be torn down), so source the return value straight from its live register
+ * when cached and drop the cache without spilling. This removes the
+ * per-function `stur <result>,[home]; ldur x0,[home]` round-trip and the dead
+ * writebacks the old flush-all emitted. A non-cached value (multi-part /
+ * aggregate / memory-required) is already authoritative in its home. */
if (value != CG_LOCAL_NONE) {
- loc = nd_loc_frame(d, value, 0);
+ NativeDirectLocal* l = nd_local(d, value);
+ loc = l->reg != REG_NONE
+ ? native_loc_reg(l->type, (NativeAllocClass)l->cls, l->reg)
+ : nd_loc_frame(d, value, 0);
locp = &loc;
}
+ nd_drop_all(d);
ND_REQUIRE_NATIVE(d, plan_ret, "target does not plan returns");
d->native->plan_ret(d->native, d->func, locp, &rets, &nrets);
for (u32 i = 0; i < nrets; ++i)