o1_inline_cap.sh (5686B)
1 #!/usr/bin/env bash 2 # Inline-pressure cap (O1.md W4). 3 # 4 # At -O1 there is no SSA post-inline cleanup, so the whole-program inliner 5 # (opt_inline) backs off when the estimated post-inline live-set 6 # (caller pregs + callee pregs) would sit past INLINE_PRESSURE_CAP (512). The cap 7 # is set far above any target's register file, so it only fires on callers that 8 # are already spilling heavily; a small, low-pressure caller still inlines the 9 # exact same callee. This is one target-independent decision, so the structural 10 # checks run identically for aarch64, x86_64, and riscv64. 11 # 12 # Green: 13 # 1. A non-tiny callee (cost above the streaming tiny cap, so only the 14 # whole-program inliner handles it) fuses into a SMALL caller — no call 15 # left, proving the cap does not over-fire on ordinary code. 16 # 2. The SAME callee called from a HUGE / high-pressure caller is NOT fused — 17 # the call survives, proving the cap backs off past its threshold. 18 # 3. An always_inline callee fuses even into the huge caller (ALWAYS bypasses 19 # the cap, matching the cost/growth gates). 20 set -euo pipefail 21 22 ROOT="$(cd "$(dirname "$0")/../.." && pwd)" 23 KIT="${KIT:-$ROOT/build/kit}" 24 WORK="$ROOT/build/test/opt/o1_inline_cap" 25 mkdir -p "$WORK" 26 27 call_mnemonics='\b(bl|blr|callq?|jalr?)\b' 28 29 # Count calls inside one function body. The disassembly has no blank separators, 30 # so walk from the function's `<name>:` header to the next symbol header. 31 calls_in() { # $1=disfile $2=funcname 32 awk -v f="<$2>:" '$0 ~ f {g = 1; next} /<[A-Za-z_].*>:/ {g = 0} g' "$1" \ 33 | grep -cE "$call_mnemonics" || true 34 } 35 36 # A callee whose cost sits above the streaming tiny-inline cap (8) but below the 37 # DEFAULT whole-program cap (20): only the whole-program inliner can fuse it, so 38 # this isolates the W4 cap on opt_inline. 39 HELPER='static int helper(int x){ int a=x*3; int b=a+7; int c=b^a;'\ 40 ' int d=c*x; int e=d-b; return a+b+c+d+e+x; }' 41 # always_inline twin: must fuse regardless of pressure. 42 AHELPER='__attribute__((always_inline)) static int ahelper(int x){'\ 43 ' int a=x*3; int b=a+7; int c=b^a; int d=c*x; int e=d-b;'\ 44 ' return a+b+c+d+e+x; }' 45 46 # Emit a caller that holds ~N*2 temporaries live across the helper call, pushing 47 # the inline-time pregs estimate well past the 512 cap. N=300 -> ~600 pregs. 48 emit_huge_caller() { # $1=name $2=callee $3=N 49 printf 'int %s(int *p){\n int s = %s(p[0])' "$1" "$2" 50 local i 51 for ((i = 0; i < $3; i++)); do printf ' + (p[%d]*%d ^ p[%d])' "$i" "$((i + 1))" "$((i + 2))"; done 52 printf ';\n return s;\n}\n' 53 } 54 55 build_src() { # writes the test TU to $1 56 { 57 printf '%s\n' "$HELPER" 58 printf '%s\n' "$AHELPER" 59 # Small, low-pressure caller of the plain helper: must fuse. 60 printf 'int small(int *p){ return helper(p[0]) + p[1]; }\n' 61 # Huge, high-pressure caller of the plain helper: must NOT fuse. 62 emit_huge_caller huge helper 300 63 # Huge caller of the always_inline helper: must fuse despite pressure. 64 emit_huge_caller huge_always ahelper 300 65 } > "$1" 66 } 67 68 check_arch() { 69 local triple=$1 tag=$2 70 local src="$WORK/$tag.c" obj="$WORK/$tag.o" dis="$WORK/$tag.dis" 71 build_src "$src" 72 "$KIT" cc -target "$triple" -O1 -ffreestanding -std=c11 -c "$src" -o "$obj" \ 73 > "$WORK/$tag.cc.out" 2>&1 74 "$KIT" objdump -d "$obj" > "$dis" 2>&1 75 76 local cs ch ca 77 cs=$(calls_in "$dis" small) 78 ch=$(calls_in "$dis" huge) 79 ca=$(calls_in "$dis" huge_always) 80 81 if [ "$cs" -ne 0 ]; then 82 printf 'o1-inline-cap FAILED: %s small caller did NOT inline helper (%s call[s]) — cap over-fired\n' \ 83 "$tag" "$cs" >&2 84 awk '/<small>:/{g=1;next} /<[A-Za-z_].*>:/{g=0} g' "$dis" | sed 's/^/ | /' >&2 85 exit 1 86 fi 87 if [ "$ch" -eq 0 ]; then 88 printf 'o1-inline-cap FAILED: %s huge/high-pressure caller inlined helper anyway — pressure cap did not back off\n' \ 89 "$tag" >&2 90 exit 1 91 fi 92 if [ "$ca" -ne 0 ]; then 93 printf 'o1-inline-cap FAILED: %s always_inline callee NOT fused into huge caller — cap ignored ALWAYS policy\n' \ 94 "$tag" >&2 95 exit 1 96 fi 97 printf 'o1-inline-cap %-8s small fused, huge backed off, always_inline fused\n' "$tag" 98 } 99 100 check_arch aarch64-linux-gnu aa64 101 check_arch x86_64-linux-gnu x64 102 check_arch riscv64-linux-gnu rv64 103 104 # Metric corroboration: the whole-program inliner must report a growth/pressure 105 # refusal at -O1 for the huge caller (the cap is folded into refuse_growth), and 106 # must still inline the small caller — i.e. the cap fires selectively, not 107 # blanket. Run via `kit run` so KIT_METRICS surfaces the finalize-sweep counters. 108 read -r -d '' RUN_SRC <<'EOF' || true 109 __attribute__((noinline)) static int sink(int x){ return x; } 110 EOF 111 build_src "$WORK/metric.c" 112 printf 'int main(void){ int p[512]; for(int i=0;i<512;i++) p[i]=i;'\ 113 ' return (small(p) | huge(p) | huge_always(p)) ? 0 : 0; }\n' >> "$WORK/metric.c" 114 if ! KIT_METRICS=1 "$KIT" run -O1 "$WORK/metric.c" >"$WORK/metric.out" 2>"$WORK/metric.err"; then 115 printf 'o1-inline-cap FAILED: `kit run -O1` on the metric TU did not exit 0\n' >&2 116 sed 's/^/ | /' "$WORK/metric.err" >&2 117 exit 1 118 fi 119 # Some inlining happened (the small caller), and some was refused (the huge one, 120 # via the growth/pressure bucket). 121 if ! grep -q 'opt.inline.inlined' "$WORK/metric.err"; then 122 printf 'o1-inline-cap FAILED: opt.inline.inlined metric absent at -O1\n' >&2 123 sed -n '1,80p' "$WORK/metric.err" >&2 124 exit 1 125 fi 126 if ! grep -q 'opt.inline.refuse_growth' "$WORK/metric.err"; then 127 printf 'o1-inline-cap FAILED: no growth/pressure refusal recorded (cap never fired)\n' >&2 128 sed -n '1,80p' "$WORK/metric.err" >&2 129 exit 1 130 fi 131 printf 'o1-inline-cap run inlined some, refused the over-pressure caller, exit 0\n' 132 133 printf 'o1-inline-cap: ok\n'