aa64_x29_bottom.sh (6572B)
1 #!/usr/bin/env bash 2 # Structural checks for the -O1 aa64 uniform x29-at-bottom known-frame layout 3 # (doc/plan/O1.md W1.1). 4 # 5 # At -O1 the frame is fully known before the body emits, so aa64 anchors x29 at 6 # the BOTTOM of the static slots (just above the outgoing-arg area). Every spill 7 # / local slot is then a one-instruction POSITIVE `ldr/str [x29,#k]` within the 8 # 32 KB scaled reach (`add x16,x29,#hi ; ldr [x16,#lo]` only past it). This 9 # replaces the old `sub x17,x29,#k ; ldur` two-instruction fallback that was 36% 10 # of lvm / 12% of sqlite instructions. 11 # 12 # The saved x29/x30 pair sits AT [x29]/[x29+8] (the frame-pointer chain kit's 13 # unwinder / __kit_backtrace walks) and outgoing args stay sp-relative 14 # ([sp,#k]) so calls after an alloca still address their arg area at current sp. 15 # 16 # These checks pin the resulting disassembly on aarch64 (the reference backend): 17 # 1. a big-frame, high-pressure function spills via `ldr/str [x29,#k]` and 18 # emits NO `sub xN,x29,#k` for spill addressing; 19 # 2. an alloca function (sp floats) still addresses spills via [x29,#k] and 20 # never recomputes a slot address with `sub xN,x29,#k`; 21 # 3. a >32 KB-frame function uses the add-build (`add x16,x29,#... ; ldr/str`) 22 # and never `sub xN,x29,#`; 23 # 4. a signed-narrow-load function never uses `sub xN,x29,#` for spills. 24 set -euo pipefail 25 26 ROOT="$(cd "$(dirname "$0")/../.." && pwd)" 27 KIT="${KIT:-$ROOT/build/kit}" 28 WORK="$ROOT/build/test/opt/aa64_x29_bottom" 29 rm -rf "$WORK" 30 mkdir -p "$WORK" 31 32 fail() { 33 printf 'aa64-x29-bottom check FAILED: %s\n' "$1" >&2 34 if [ -n "${2:-}" ] && [ -f "$2" ]; then 35 sed 's/^/ | /' "$2" >&2 36 fi 37 exit 1 38 } 39 40 slice_func() { 41 local src="$1" func="$2" out="$3" 42 awk -v name="$func" ' 43 $0 ~ "^[0-9a-f]+ <" name ">:" { in_fn = 1; print; next } 44 /^[0-9a-f]+ </ { in_fn = 0 } 45 in_fn { print } 46 ' "$src" > "$out" 47 } 48 49 compile_case() { 50 local name="$1" src="$2" 51 "$KIT" cc -target aarch64-linux-gnu -O1 -std=c11 -c "$src" \ 52 -o "$WORK/$name.o" > "$WORK/$name.cc.out" 2> "$WORK/$name.cc.err" || 53 { cat "$WORK/$name.cc.err" >&2; fail "compile of $name failed"; } 54 "$KIT" objdump -d "$WORK/$name.o" \ 55 > "$WORK/$name.dis" 2> "$WORK/$name.objdump.err" 56 } 57 58 # A spill never recomputes its address with `sub xN, x29, #k` on the known-frame 59 # path -- that is exactly the 2-insn pattern W1.1 removes. 60 no_frame_sub() { # $1 = sliced fn file, $2 = label 61 if grep -Eq 'sub[[:space:]]+x[0-9]+, x29, #' "$1"; then 62 fail "$2 recomputes a frame-slot address with 'sub xN, x29, #' (W1.1 should address [x29,#k] positively)" "$1" 63 fi 64 } 65 66 # ---- fixtures ---- 67 68 # 1. Big frame (>256-byte spill area) + outgoing stack args (>8-arg calls). This 69 # is the case W1.1 targets: a large frame means spill offsets escape `ldur`'s 70 # +-256 range, and out_stack>0 disqualifies the old folded bottom-record. The 71 # baseline emits `sub x17,x29,#k ; ldur` for each such spill; x29-at-bottom 72 # must address every one as a positive [x29,#k] with no frame-address `sub`. 73 cat > "$WORK/pressure.c" <<'EOF' 74 extern long sink(long,long,long,long,long,long,long,long,long,long); 75 long pressure(long *p) { 76 long v[48]; 77 for (int i = 0; i < 48; i++) v[i] = p[i] + i; 78 long s = 0; 79 s += sink(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8],v[9]); 80 s += sink(v[10],v[11],v[12],v[13],v[14],v[15],v[16],v[17],v[18],v[19]); 81 s += sink(v[20],v[21],v[22],v[23],v[24],v[25],v[26],v[27],v[28],v[29]); 82 s += sink(v[30],v[31],v[32],v[33],v[34],v[35],v[36],v[37],v[38],v[39]); 83 for (int i = 0; i < 48; i++) s += v[i]; 84 return s; 85 } 86 EOF 87 compile_case pressure "$WORK/pressure.c" 88 slice_func "$WORK/pressure.dis" pressure "$WORK/pressure.fn" 89 [ -s "$WORK/pressure.fn" ] || fail "pressure not found in disassembly" "$WORK/pressure.dis" 90 no_frame_sub "$WORK/pressure.fn" pressure 91 grep -Eq '(ldr|str)[[:space:]]+[wx][0-9]+, \[x29, #[0-9]+\]' "$WORK/pressure.fn" || 92 fail "pressure has no positive [x29,#k] spill access (expected x29-at-bottom)" "$WORK/pressure.fn" 93 # The prologue must anchor x29 = sp + os (saved fp/lr pair at [x29]/[x29+8]). 94 grep -Eq 'add[[:space:]]+x29, sp' "$WORK/pressure.fn" || 95 fail "pressure prologue did not anchor x29 = sp + os" "$WORK/pressure.fn" 96 97 # 2. alloca: sp floats during the body, but x29 stays put, so spills are still 98 # [x29,#k] and outgoing args are [sp,#k]. (The call below takes stack args.) 99 cat > "$WORK/alloca_fn.c" <<'EOF' 100 extern long use(long *, long, long, long, long, long, long, long, long, long); 101 long alloca_fn(long n, long x) { 102 long *p = __builtin_alloca(n * sizeof(long)); 103 for (long i = 0; i < n; i++) p[i] = x + i; 104 long r = use(p, x, x+1, x+2, x+3, x+4, x+5, x+6, x+7, x+8); 105 return r + p[0]; 106 } 107 EOF 108 compile_case alloca_fn "$WORK/alloca_fn.c" 109 slice_func "$WORK/alloca_fn.dis" alloca_fn "$WORK/alloca_fn.fn" 110 [ -s "$WORK/alloca_fn.fn" ] || fail "alloca_fn not found in disassembly" "$WORK/alloca_fn.dis" 111 no_frame_sub "$WORK/alloca_fn.fn" alloca_fn 112 grep -Eq 'add[[:space:]]+x29, sp' "$WORK/alloca_fn.fn" || 113 fail "alloca_fn prologue did not anchor x29 = sp + os" "$WORK/alloca_fn.fn" 114 115 # 3. >32 KB frame: the scaled `ldr/str [x29,#k]` reach is exceeded, so the slot 116 # address is built with `add x16/x17, x29, #hi` -- never `sub xN, x29, #k`. 117 cat > "$WORK/huge.c" <<'EOF' 118 extern void use_buf(volatile char *, volatile char *); 119 long huge(long x) { 120 volatile char a[40000]; 121 volatile char b[8]; 122 a[0] = (char)x; 123 b[0] = (char)(x + 1); 124 use_buf(a, b); 125 return a[0] + b[0]; 126 } 127 EOF 128 compile_case huge "$WORK/huge.c" 129 slice_func "$WORK/huge.dis" huge "$WORK/huge.fn" 130 [ -s "$WORK/huge.fn" ] || fail "huge not found in disassembly" "$WORK/huge.dis" 131 no_frame_sub "$WORK/huge.fn" huge 132 grep -Eq 'add[[:space:]]+x1[67], x29, #' "$WORK/huge.fn" || 133 fail "huge >32KB frame did not build a far slot address with 'add x16/x17, x29, #'" "$WORK/huge.fn" 134 135 # 4. signed narrow loads in a big frame: spilled narrow values still address 136 # [x29,#k] (ldrsb when reloaded as signed), never a sub-x29 recompute. 137 cat > "$WORK/snl.c" <<'EOF' 138 extern long sink2(long,long,long,long,long,long,long,long,long,long); 139 long snl(signed char *p) { 140 signed char v[400]; 141 for (int i = 0; i < 400; i++) v[i] = p[i]; 142 long s = sink2(v[0],v[40],v[80],v[120],v[160],v[200],v[240],v[280],v[320],v[360]); 143 for (int i = 0; i < 400; i += 17) s += v[i]; 144 return s; 145 } 146 EOF 147 compile_case snl "$WORK/snl.c" 148 slice_func "$WORK/snl.dis" snl "$WORK/snl.fn" 149 [ -s "$WORK/snl.fn" ] || fail "snl not found in disassembly" "$WORK/snl.dis" 150 no_frame_sub "$WORK/snl.fn" snl 151 152 printf 'aa64-x29-bottom: ok (pressure, alloca, >32KB add-build, signed narrow)\n'