commit f61d252b3345168dc0d9706cb48ce485f10502cb
parent 98b1f010ea090a8d18360d194880ea658b26296c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 13 Jun 2026 07:15:36 -0700
perf(cg): -O0 register-resident call result — drop the result home round-trip
The post-call result still round-tripped: plan_ret moved the ABI result register
to the result temp's frame home (`bl; stur w0,[home]`) and the consumer reloaded
it. Symmetric to the arg side, keep a scalar result register-resident: after the
selective flush, claim a cache register for the result local (pinned across the
callee materialize so it cannot be evicted while it holds garbage), have the
post-call ret move write the ABI register into it, and mark it cached+dirty so
the consumer reads it directly. A non-cacheable result (aggregate / multi-part /
sret) keeps the home path.
viacall: bl _g; stur w0,[home]; ldur w9,[home] -> bl _g; mov w11,w0
sqlite3.c -c -O0 (arm64-macOS): emitted insns 479,263 -> 474,752 (-4,511);
object 2,311,552 -> 2,293,504 B; compile instrs flat (within noise).
Byte-deterministic.
Green: test-toy 1392/0, test-parse 3920+129/0, test-smoke-x64 3/0,
test-smoke-rv64 3/0, test-cg-api 0 fail, test-libc musl 18/0 + glibc 9/0;
sqlite e2e 84|2; call probe matches clang.
Diffstat:
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c
@@ -1772,13 +1772,36 @@ static void nd_call(CgTarget* t, const CGCallDesc* desc) {
args[i] = nd_loc_frame(d, desc->args[i], 0);
}
}
- if (nresults) results[0] = nd_loc_frame(d, desc->result, 0);
/* An indirect callee is read from its home; make it authoritative before the
* selective flush (the cg only ever produces an OPK_LOCAL or OPK_GLOBAL
* callee). */
if (desc->callee.kind == OPK_LOCAL) nd_flush_local(d, desc->callee.v.local);
/* Spill the live-across set: everything cached except the kept (dead) args. */
nd_flush_all_except_kept_args(d, desc);
+ /* Keep a scalar result register-resident: claim a cache register for it now
+ * (the cache holds only the pinned kept args) and have the post-call ret move
+ * write the ABI result register into it, instead of storing the result to its
+ * home and reloading it at the consumer. The claimed register holds garbage
+ * until that ret move, but nothing reads the result before then. A
+ * non-cacheable result (aggregate / multi-part / sret) keeps the home path. */
+ if (nresults) {
+ NativeDirectLocal* rl = nd_local(d, desc->result);
+ Reg r = nd_local_cacheable(d, rl)
+ ? nd_cache_alloc(d, (NativeAllocClass)rl->cls)
+ : REG_NONE;
+ if (r != REG_NONE) {
+ d->reg_owner[rl->cls][r] = desc->result;
+ rl->reg = r;
+ rl->dirty = 1;
+ nd_cache_link(d, desc->result);
+ /* Pin until the post-call ret move so the callee materialize cannot evict
+ * this not-yet-valid entry as a scratch victim. */
+ d->scratch_used[rl->cls] |= 1u << r;
+ results[0] = native_loc_reg(rl->type, (NativeAllocClass)rl->cls, r);
+ } else {
+ results[0] = nd_loc_frame(d, desc->result, 0);
+ }
+ }
nd_barrier(d, NATIVE_DIRECT_BARRIER_CALL | NATIVE_DIRECT_BARRIER_MEMORY);
nd.fn_type = desc->fn_type;
nd.callee = nd_loc_operand(d, desc->callee);
@@ -1822,6 +1845,11 @@ static void nd_call(CgTarget* t, const CGCallDesc* desc) {
}
for (u32 i = 0; i < plan.nrets; ++i)
nd_write_loc(d, plan.rets[i].dst, plan.rets[i].src, plan.rets[i].mem);
+ /* The result's cache register is now valid; unpin it so it behaves as an
+ * ordinary write-back cache entry (its value reaches the home on the next
+ * flush, or is dropped if the result turns out dead). */
+ if (nresults && results[0].kind == NATIVE_LOC_REG)
+ d->scratch_used[results[0].cls] &= ~(1u << results[0].v.reg);
if (release_callee_tmp)
nd_scratch_release(d, (NativeAllocClass)callee_tmp.cls, callee_tmp.v.reg);
}