rv64_far_slot.sh (6532B)
1 #!/usr/bin/env bash 2 # Structural check + measurement lock-in for rv64 far-slot frame addressing 3 # (doc/plan/O1.md #### W1.2). 4 # 5 # THE W1.2 FINDING (measure-first). rv64's imm12 displacement window is +/-2 KB 6 # -- 8x aarch64's `ldur` +/-256 -- so a frame slot is addressed by a SINGLE 7 # instruction `ld/sd off(s0)` for any realistic hot working set. Only a frame 8 # whose slot offsets exceed +/-2047 needs the 3-instruction far build 9 # (`lui; addiw; add t0,s0,t1; ld 0(t0)`, src/arch/riscv/native.c rv_resolve_mem_addr). 10 # W1.0 hot-slot-low ordering already hands the hottest spills the smallest 11 # offsets, so what little far-slot traffic remains is the COLD tail of a few 12 # huge-frame functions, never the hot loop. 13 # 14 # Corpus measurement (kit -O1 -target riscv64-linux-gnu, lvm.c / sqlite3.c / 15 # miniz.c) that motivated leaving W1.2 unimplemented: 16 # lvm.c : 14,176 insns, 0 far-builds, 6,984 single-insn (s0) accesses 17 # sqlite3.c: 271,847 insns, 0 far-builds, 64,091 single-insn (s0) accesses 18 # miniz.c : 36,727 insns, 265 far-builds (0.72%), 9,959 single-insn 19 # -- all 265 confined to ~12 functions carrying multi-KB on-stack 20 # decompressor/zip structs; the bulk are cold prologue/epilogue 21 # register saves, only ~5 are W1a-CSE-eliminable back-to-back 22 # rebuilds (the rest are split by calls that clobber t0/t1). 23 # sqlite's interpreter -- the heaviest spiller, 35,426 frame-address `sub`s on 24 # aarch64 -- has ZERO rv64 far traffic: the wide window swallows every frame. 25 # Conclusion: no residual far-slot work is warranted on rv64. 26 # 27 # This guard pins that conclusion so a future regression (a layout change that 28 # pushes hot slots past the imm12 window, or a frame-addressing change that stops 29 # emitting single-instruction (s0) accesses) is caught: realistic hot-loop and 30 # call-heavy spill functions must address their frame slots single-instruction, 31 # with NO far-slot `lui;...;add` build in the hot body. 32 # 33 # Gate is correctness, not byte-identity. Debug build emits byte-identical code 34 # to release, so build/kit is sufficient. 35 set -euo pipefail 36 37 ROOT="$(cd "$(dirname "$0")/../.." && pwd)" 38 KIT="${KIT:-$ROOT/build/kit}" 39 WORK="$ROOT/build/test/opt/rv64_far_slot" 40 mkdir -p "$WORK" 41 42 TRIPLE=riscv64-linux-gnu 43 44 fail() { 45 printf 'rv64_far_slot FAILED: %s\n' "$1" >&2 46 if [ -n "${2:-}" ] && [ -f "$WORK/$2.dis" ]; then 47 printf ' --- %s disassembly (head) ---\n' "$2" >&2 48 sed -n '1,80p' "$WORK/$2.dis" | sed 's/^/ | /' >&2 49 fi 50 exit 1 51 } 52 53 build_dis() { # $1 = src, $2 = tag 54 local src="$1" tag="$2" 55 "$KIT" cc -target "$TRIPLE" -O1 -std=c11 -ffreestanding -nostdinc \ 56 -I"$ROOT/rt/include" -c "$src" -o "$WORK/$tag.o" > "$WORK/$tag.cc.out" 2>&1 \ 57 || { printf 'rv64_far_slot FAILED: %s cc failed\n' "$tag" >&2 58 sed 's/^/ | /' "$WORK/$tag.cc.out" >&2; exit 1; } 59 "$KIT" objdump -d "$WORK/$tag.o" > "$WORK/$tag.dis" 2>&1 60 } 61 62 # A far-slot frame-address build is an `add rd, s0, tX` (s0-relative address that 63 # overflowed imm12); the single-instruction in-range form is `ld/sd reg, off(s0)`. 64 count_far() { grep -cE ' add[w]?[[:space:]]+[a-z0-9_]+, s0, (t0|t1)' "$WORK/$1.dis" || true; } 65 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; } 66 67 # ---- Probe A: a hot interpreter-style loop with a real spilling working set --- 68 # Mirrors the lvm.c luaV_execute shape: a 688-byte frame whose spills all sit 69 # within imm12. Many accumulators (more than the GP file) updated every iteration 70 # force spills; the frame stays well under +/-2 KB, so EVERY spill access must be 71 # single-instruction and NO far-slot build may appear. 72 SRC_A="$WORK/hot_loop.c" 73 { 74 echo 'typedef unsigned long long u64;' 75 echo 'extern u64 sink(u64);' 76 echo 'u64 hot_loop(const u64 *in, int n) {' 77 for i in $(seq 0 23); do echo " u64 h$i = in[$i];"; done 78 echo ' for (int i = 0; i < n; i++) {' 79 echo ' u64 x = in[i & 63];' 80 for i in $(seq 0 23); do echo " h$i = (h$i ^ x) + h$(((i + 1) % 24)) * 3u;"; done 81 echo ' }' 82 printf ' return' 83 for i in $(seq 0 23); do printf ' h%d ^' "$i"; done 84 echo ' 0;' 85 echo '}' 86 } > "$SRC_A" 87 88 build_dis "$SRC_A" hot_loop 89 a_far="$(count_far hot_loop)" 90 a_single="$(count_single hot_loop)" 91 [ "${a_single:-0}" -ge 16 ] \ 92 || fail "hot_loop: expected many single-instruction (s0) spill accesses (single=$a_single)" hot_loop 93 [ "${a_far:-0}" -eq 0 ] \ 94 || fail "hot_loop: a moderate hot working set produced a far-slot build (far=$a_far) -- imm12 window regressed?" hot_loop 95 96 # ---- Probe B: a large but in-range frame (cold scalars push toward the edge) -- 97 # ~90 live-across-call scalars inflate the frame to ~1.6 KB -- approaching, but 98 # staying within, the +/-2 KB imm12 window -- with a hot accumulator loop on top. 99 # This is the realistic boundary case: a substantial frame that the wide rv64 100 # window still addresses single-instruction. W1.0 keeps the hot slots low, so the 101 # hot loop stays single-instruction and the WHOLE function emits no far build. 102 # (Push the scalar count past ~120 and the frame exceeds 2 KB, at which point 103 # far builds legitimately appear in the COLD prologue -- the doc's expected 104 # residual tail; that is not what this guard pins.) 105 SRC_B="$WORK/big_frame.c" 106 { 107 echo 'typedef unsigned long long u64;' 108 echo 'extern u64 sink(u64 *);' 109 echo 'u64 big_frame(const u64 *in, int n) {' 110 for i in $(seq 0 15); do echo " u64 h$i = in[$i];"; done 111 for i in $(seq 0 89); do echo " u64 k$i = in[$((i % 64))] + ${i}u;"; done 112 echo ' for (int i = 0; i < n; i++) {' 113 echo ' u64 x = in[i & 63];' 114 for i in $(seq 0 15); do echo " h$i = (h$i ^ x) + h$(((i + 1) % 16));"; done 115 echo ' }' 116 echo ' u64 ks[90];' 117 for i in $(seq 0 89); do echo " ks[$i] = k$i;"; done 118 echo ' sink(ks);' 119 printf ' return' 120 for i in $(seq 0 15); do printf ' h%d ^' "$i"; done 121 echo ' 0;' 122 echo '}' 123 } > "$SRC_B" 124 125 build_dis "$SRC_B" big_frame 126 b_far="$(count_far big_frame)" 127 b_single="$(count_single big_frame)" 128 [ "${b_single:-0}" -ge 16 ] \ 129 || fail "big_frame: expected single-instruction (s0) hot accesses (single=$b_single)" big_frame 130 [ "${b_far:-0}" -eq 0 ] \ 131 || fail "big_frame: an in-range (<2KB) frame produced a far-slot build (far=$b_far)" big_frame 132 133 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' \ 134 "$a_single" "$a_far" "$b_single" "$b_far"