kit

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

commit d3493a9dc57891958726cc00c1bb57d6a76f0c58
parent ba542bfde60e045ffc887bfdcb11f8006a406eef
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 17:54:03 -0700

perf(cg): -O0 fp args into fa-regs + result-in-a0 (Lever 1/5, riscv)

Front fa0..fa7 in the rv NDT fp value-cache pool (rv_fp_allocable) so the
-O0 forward-materialization (api_pack_call_args_in_order, A1) lands fp call
args directly in their ABI registers, mirroring the int pool a0..a7. Flip
ndt_result_reg_stable for riscv: a0/fa0 are clobbered only at call
boundaries (rv div/rem/mul are R-type with explicit operands, unlike
x86-64 implicit-RAX), so a scalar call result can stay cached in a0/fa0
across straight-line ops and feed the next consumer with no mov, as on
aa64. ft0/ft1 (emit) and ft2/ft3 (driver) stay out of the pool.

Gate (run-correctness + determinism, this is a codesize-track change):
- make bin RELEASE=1: clean (-Werror, no warnings)
- test-smoke-rv64: 3 pass / 0 fail; test-toy: 1392 pass / 0 fail / 35 skip
  (skips pre-existing C-target/wasm/inline-asm, unrelated to rv64)
- functional rv64 run (computed args, shared temp, result-stable-then-div,
  nested-call args, + fp variants) compiled by kit, run on real riscv64
  (qemu-user in podman riscv64/debian): exit 0 = all 7 checks pass; clang
  build of same test also exits 0 (expected values correct); negative
  control returns 1 (harness detects an injected failure)
- determinism: object byte-identical across two compiles (two sources)

Measured (call-heavy corpus, kit -c -target riscv64-linux -O0):
  .text 1644->1564 B (-4.9%), object 3512->3432 B, total insns 411->391
  (-4.9%); fsgnj.d (fp reg-reg mov / fmv.d pseudo) 37->21 (-43%, fp-front),
  mv (int reg-reg) 34->30 (-12%, result-in-a0).

Diffstat:
Msrc/arch/riscv/native.c | 24+++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)

diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -495,15 +495,18 @@ static const NativePhysRegInfo rv_int_phys[] = { /* NDT (-O0) fp value-cache / scratch pool (read only by NativeDirectTarget; the * optimizer uses the phys[] ALLOCABLE flags, which also cover the callee-saved - * fs0..fs11). Caller-saved only — ft4..ft7, ft8..ft11, and the fa0..fa7 arg - * registers — for the same reasons as the int pool: the -O0 cache flushes at + * fs0..fs11). Caller-saved only — the fa0..fa7 arg registers, then ft4..ft7 and + * ft8..ft11 — for the same reasons as the int pool: the -O0 cache flushes at * every call/branch/return, so caller-saved suffices, and keeping callee-saves * out of the pool keeps the fixed single-pass prologue placeholder from - * overflowing. ft0/ft1 reserved as emit-internal scratch; ft2/ft3 driver - * scratch. */ -static const Reg rv_fp_allocable[] = {4u, 5u, 6u, 7u, 28u, 29u, - 30u, 31u, 10u, 11u, 12u, 13u, - 14u, 15u, 16u, 17u}; /* ft4-7,ft8-11,fa0-7 */ + * overflowing. fa0..fa7 come FIRST (mirroring the int pool's a0..a7): + * nd_cache_alloc scans the pool in order, so fronting the arg registers lets a + * producer materialize a call's fp args directly into their ABI registers + * (Lever 1, via api_pack_call_args_in_order). ft0/ft1 reserved as emit-internal + * scratch; ft2/ft3 driver scratch. */ +static const Reg rv_fp_allocable[] = {10u, 11u, 12u, 13u, 14u, 15u, + 16u, 17u, 4u, 5u, 6u, 7u, + 28u, 29u, 30u, 31u}; /* fa0-7,ft4-7,ft8-11 */ static const Reg rv_fp_scratch[] = {2u, 3u}; /* ft2, ft3 */ static const NativePhysRegInfo rv_fp_phys[] = { @@ -632,6 +635,13 @@ static const NativeRegInfo rv_reg_info = { * ft/fa regs (fp), so the -O0 path never needs a callee-saved register — * enabling the tcc-style deferred-`sub` prologue (see rv_func_begin). */ .ndt_caller_saved_only = 1u, + /* a0/fa0 are clobbered only by calls: rv div/rem/mul are R-type with + * explicit operands (unlike x86-64's implicit-RAX div/mul), so a scalar + * call result can stay cached in a0/fa0 across the following straight-line + * ops and feed the next consumer with no mov — exactly as on aa64 (cf. the + * aa64 ndt_result_reg_stable comment; doc/plan/PERF-O0-CODESIZE.md Lever 5). + * The NDT places the result directly in the ABI result reg post-call. */ + .ndt_result_reg_stable = 1u, .resolve_name = rv_resolve_name, .asm_operand_reg_ok = rv_asm_operand_reg_ok, .asm_constraint_reg = rv_asm_constraint_reg,