kit

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

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:
Mmk/test.mk | 9++++++++-
Atest/opt/rv64_far_slot.sh | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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"