kit

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

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:
Msrc/cg/native_direct_target.c | 30+++++++++++++++++++++++++++++-
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); }