kit

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

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"