commit a1ccdbcb9b22194c1ff989e6747755409db4efc3
parent d4eeb8400c5efb6c824ffa48eba4f7d24b823279
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 00:21:50 -0700
rv64: assess far-slot traffic at O1 (zero in hot loops; +guard) (O1.md W1.2)
Diffstat:
2 files changed, 142 insertions(+), 1 deletion(-)
diff --git a/mk/test.mk b/mk/test.mk
@@ -859,7 +859,7 @@ test-macho: lib $(TEST_RT_DEP) $(ROUNDTRIP_BIN_MACHO) $(LINK_EXE_RUNNER) $(JIT_R
OPT_TEST_BIN = build/test/cg_ir_lower_test
TINY_INLINE_TEST_BIN = build/test/tiny_inline_test
-test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom test-opt-o1-coalesce test-opt-o1-switch-imm test-opt-o1-inline-cap
+test-opt: bin $(OPT_TEST_BIN) test-opt-tiny-inline test-opt-inline test-opt-zero-arg test-opt-static-prune-aa64 test-opt-aa64-tail test-opt-x64-win-tail-sret test-opt-prologue-tier test-opt-whole-program-inline test-opt-lto-phase1 test-opt-redundant-copy-ext test-opt-redundant-frame-sub test-opt-o1-branch-cleanup test-opt-hot-slot-order test-opt-o1-remat test-opt-aa64-x29-bottom test-opt-o1-coalesce test-opt-o1-switch-imm test-opt-o1-inline-cap test-opt-rv64-far-slot
$(OPT_TEST_BIN)
@@ -917,6 +917,13 @@ test-opt-o1-switch-imm: bin
test-opt-o1-inline-cap: bin
@KIT=$(abspath $(BIN)) bash test/opt/o1_inline_cap.sh
+# Structural disasm check (rv64): after W1.0 hot-slot ordering, hot frame slots
+# are single-instruction ld/sd off(s0) within the imm12 window -- no far-slot
+# lui;addiw;add rebuilds in hot code (O1.md W1.2 -- assessment, no codegen change).
+.PHONY: test-opt-rv64-far-slot
+test-opt-rv64-far-slot: bin
+ @KIT=$(abspath $(BIN)) bash test/opt/rv64_far_slot.sh
+
test-opt-tiny-inline: bin $(TINY_INLINE_TEST_BIN)
$(TINY_INLINE_TEST_BIN)
diff --git a/test/opt/rv64_far_slot.sh b/test/opt/rv64_far_slot.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+# Structural check + measurement lock-in for rv64 far-slot frame addressing
+# (doc/plan/O1.md #### W1.2).
+#
+# THE W1.2 FINDING (measure-first). rv64's imm12 displacement window is +/-2 KB
+# -- 8x aarch64's `ldur` +/-256 -- so a frame slot is addressed by a SINGLE
+# instruction `ld/sd off(s0)` for any realistic hot working set. Only a frame
+# whose slot offsets exceed +/-2047 needs the 3-instruction far build
+# (`lui; addiw; add t0,s0,t1; ld 0(t0)`, src/arch/riscv/native.c rv_resolve_mem_addr).
+# W1.0 hot-slot-low ordering already hands the hottest spills the smallest
+# offsets, so what little far-slot traffic remains is the COLD tail of a few
+# huge-frame functions, never the hot loop.
+#
+# Corpus measurement (kit -O1 -target riscv64-linux-gnu, lvm.c / sqlite3.c /
+# miniz.c) that motivated leaving W1.2 unimplemented:
+# lvm.c : 14,176 insns, 0 far-builds, 6,984 single-insn (s0) accesses
+# sqlite3.c: 271,847 insns, 0 far-builds, 64,091 single-insn (s0) accesses
+# miniz.c : 36,727 insns, 265 far-builds (0.72%), 9,959 single-insn
+# -- all 265 confined to ~12 functions carrying multi-KB on-stack
+# decompressor/zip structs; the bulk are cold prologue/epilogue
+# register saves, only ~5 are W1a-CSE-eliminable back-to-back
+# rebuilds (the rest are split by calls that clobber t0/t1).
+# sqlite's interpreter -- the heaviest spiller, 35,426 frame-address `sub`s on
+# aarch64 -- has ZERO rv64 far traffic: the wide window swallows every frame.
+# Conclusion: no residual far-slot work is warranted on rv64.
+#
+# This guard pins that conclusion so a future regression (a layout change that
+# pushes hot slots past the imm12 window, or a frame-addressing change that stops
+# emitting single-instruction (s0) accesses) is caught: realistic hot-loop and
+# call-heavy spill functions must address their frame slots single-instruction,
+# with NO far-slot `lui;...;add` build in the hot body.
+#
+# Gate is correctness, not byte-identity. Debug build emits byte-identical code
+# to release, so build/kit is sufficient.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+KIT="${KIT:-$ROOT/build/kit}"
+WORK="$ROOT/build/test/opt/rv64_far_slot"
+mkdir -p "$WORK"
+
+TRIPLE=riscv64-linux-gnu
+
+fail() {
+ printf 'rv64_far_slot FAILED: %s\n' "$1" >&2
+ if [ -n "${2:-}" ] && [ -f "$WORK/$2.dis" ]; then
+ printf ' --- %s disassembly (head) ---\n' "$2" >&2
+ sed -n '1,80p' "$WORK/$2.dis" | sed 's/^/ | /' >&2
+ fi
+ exit 1
+}
+
+build_dis() { # $1 = src, $2 = tag
+ local src="$1" tag="$2"
+ "$KIT" cc -target "$TRIPLE" -O1 -std=c11 -ffreestanding -nostdinc \
+ -I"$ROOT/rt/include" -c "$src" -o "$WORK/$tag.o" > "$WORK/$tag.cc.out" 2>&1 \
+ || { printf 'rv64_far_slot FAILED: %s cc failed\n' "$tag" >&2
+ sed 's/^/ | /' "$WORK/$tag.cc.out" >&2; exit 1; }
+ "$KIT" objdump -d "$WORK/$tag.o" > "$WORK/$tag.dis" 2>&1
+}
+
+# A far-slot frame-address build is an `add rd, s0, tX` (s0-relative address that
+# overflowed imm12); the single-instruction in-range form is `ld/sd reg, off(s0)`.
+count_far() { grep -cE ' add[w]?[[:space:]]+[a-z0-9_]+, s0, (t0|t1)' "$WORK/$1.dis" || true; }
+count_single() { grep -cE ' (ld|sd|lw|sw|lh|sh|lb|sb|lbu|lhu|lwu|fld|flw|fsd|fsw)[[:space:]]+[a-z0-9_]+, -?[0-9]+\(s0\)' "$WORK/$1.dis" || true; }
+
+# ---- Probe A: a hot interpreter-style loop with a real spilling working set ---
+# Mirrors the lvm.c luaV_execute shape: a 688-byte frame whose spills all sit
+# within imm12. Many accumulators (more than the GP file) updated every iteration
+# force spills; the frame stays well under +/-2 KB, so EVERY spill access must be
+# single-instruction and NO far-slot build may appear.
+SRC_A="$WORK/hot_loop.c"
+{
+ echo 'typedef unsigned long long u64;'
+ echo 'extern u64 sink(u64);'
+ echo 'u64 hot_loop(const u64 *in, int n) {'
+ for i in $(seq 0 23); do echo " u64 h$i = in[$i];"; done
+ echo ' for (int i = 0; i < n; i++) {'
+ echo ' u64 x = in[i & 63];'
+ for i in $(seq 0 23); do echo " h$i = (h$i ^ x) + h$(((i + 1) % 24)) * 3u;"; done
+ echo ' }'
+ printf ' return'
+ for i in $(seq 0 23); do printf ' h%d ^' "$i"; done
+ echo ' 0;'
+ echo '}'
+} > "$SRC_A"
+
+build_dis "$SRC_A" hot_loop
+a_far="$(count_far hot_loop)"
+a_single="$(count_single hot_loop)"
+[ "${a_single:-0}" -ge 16 ] \
+ || fail "hot_loop: expected many single-instruction (s0) spill accesses (single=$a_single)" hot_loop
+[ "${a_far:-0}" -eq 0 ] \
+ || fail "hot_loop: a moderate hot working set produced a far-slot build (far=$a_far) -- imm12 window regressed?" hot_loop
+
+# ---- Probe B: a large but in-range frame (cold scalars push toward the edge) --
+# ~90 live-across-call scalars inflate the frame to ~1.6 KB -- approaching, but
+# staying within, the +/-2 KB imm12 window -- with a hot accumulator loop on top.
+# This is the realistic boundary case: a substantial frame that the wide rv64
+# window still addresses single-instruction. W1.0 keeps the hot slots low, so the
+# hot loop stays single-instruction and the WHOLE function emits no far build.
+# (Push the scalar count past ~120 and the frame exceeds 2 KB, at which point
+# far builds legitimately appear in the COLD prologue -- the doc's expected
+# residual tail; that is not what this guard pins.)
+SRC_B="$WORK/big_frame.c"
+{
+ echo 'typedef unsigned long long u64;'
+ echo 'extern u64 sink(u64 *);'
+ echo 'u64 big_frame(const u64 *in, int n) {'
+ for i in $(seq 0 15); do echo " u64 h$i = in[$i];"; done
+ for i in $(seq 0 89); do echo " u64 k$i = in[$((i % 64))] + ${i}u;"; done
+ echo ' for (int i = 0; i < n; i++) {'
+ echo ' u64 x = in[i & 63];'
+ for i in $(seq 0 15); do echo " h$i = (h$i ^ x) + h$(((i + 1) % 16));"; done
+ echo ' }'
+ echo ' u64 ks[90];'
+ for i in $(seq 0 89); do echo " ks[$i] = k$i;"; done
+ echo ' sink(ks);'
+ printf ' return'
+ for i in $(seq 0 15); do printf ' h%d ^' "$i"; done
+ echo ' 0;'
+ echo '}'
+} > "$SRC_B"
+
+build_dis "$SRC_B" big_frame
+b_far="$(count_far big_frame)"
+b_single="$(count_single big_frame)"
+[ "${b_single:-0}" -ge 16 ] \
+ || fail "big_frame: expected single-instruction (s0) hot accesses (single=$b_single)" big_frame
+[ "${b_far:-0}" -eq 0 ] \
+ || fail "big_frame: an in-range (<2KB) frame produced a far-slot build (far=$b_far)" big_frame
+
+printf 'rv64_far_slot: OK (hot_loop single=%s far=%s; big_frame single=%s far=%s) -- hot slots are single-instruction ld/sd off(s0); no residual far-slot work warranted (O1.md W1.2)\n' \
+ "$a_single" "$a_far" "$b_single" "$b_far"