kit

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

whole_program_inline.sh (8344B)


      1 #!/usr/bin/env bash
      2 # Whole-program cross-function inlining (LTO Phase 0).
      3 #
      4 # At -O1 the optimizer defers emission to a module-wide finalize sweep that GCs
      5 # dead symbols and runs the whole-program inliner (opt_inline) over the live
      6 # FuncSet. This is one path for every arch — no arch special-casing — so the
      7 # structural checks run identically for aarch64, x86_64, and riscv64.
      8 #
      9 # Green: a small static callee fuses into its caller (no call instruction left
     10 # in the caller, and the `opt.inline.inlined` metric fires). Behavioral: the
     11 # fused program still returns the right value via the host JIT.
     12 set -euo pipefail
     13 
     14 ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
     15 KIT="${KIT:-$ROOT/build/kit}"
     16 WORK="$ROOT/build/test/opt/whole_program_inline"
     17 mkdir -p "$WORK"
     18 
     19 # A caller (`compute`) that reaches two small static helpers. Both should fuse
     20 # in, leaving `compute` call-free.
     21 read -r -d '' SRC <<'EOF' || true
     22 static int add1(int x) { return x + 1; }
     23 static int twice(int x) { return add1(add1(x)); }
     24 int compute(int x) { return twice(x) + add1(x); }
     25 EOF
     26 
     27 # Per-arch call mnemonics (aarch64 bl/blr, x86_64 call/callq, riscv jal/jalr).
     28 # After fusion `compute` must contain none of them.
     29 call_mnemonics='\b(bl|blr|callq?|jalr?)\b'
     30 
     31 check_arch() {
     32   local triple=$1
     33   local tag=$2
     34   local src="$WORK/$tag.c"
     35   local obj="$WORK/$tag.o"
     36   printf '%s\n' "$SRC" > "$src"
     37   "$KIT" cc -target "$triple" -O1 -ffreestanding -std=c11 -c "$src" \
     38     -o "$obj" > "$WORK/$tag.cc.out" 2>&1
     39   "$KIT" objdump -d "$obj" > "$WORK/$tag.dis" 2>&1
     40   # Isolate the `compute` function body and count residual calls.
     41   local ncalls
     42   ncalls=$(sed -n '/<compute>:/,/^$/p' "$WORK/$tag.dis" \
     43     | grep -cE "$call_mnemonics" || true)
     44   if [ "$ncalls" -ne 0 ]; then
     45     printf 'whole-program-inline FAILED: %s left %s call(s) in compute (callee not fused)\n' \
     46       "$tag" "$ncalls" >&2
     47     sed -n '/<compute>:/,/^$/p' "$WORK/$tag.dis" | sed 's/^/  | /' >&2
     48     exit 1
     49   fi
     50   printf 'whole-program-inline %-8s fused (compute call-free)\n' "$tag"
     51 }
     52 
     53 check_arch aarch64-linux-gnu aa64
     54 check_arch x86_64-linux-gnu  x64
     55 check_arch riscv64-linux-gnu rv64
     56 
     57 # Interposition guard: a weak callee is link-time replaceable, so inlining its
     58 # body would defeat a strong override. The caller must keep the call. Check on
     59 # every arch (one unified inliner path).
     60 read -r -d '' WEAK_SRC <<'EOF' || true
     61 __attribute__((weak)) int wcallee(int x) { return x + 1; }
     62 int wcaller(int x) { return wcallee(x); }
     63 EOF
     64 check_weak_not_inlined() {
     65   local triple=$1
     66   local tag=$2
     67   local src="$WORK/weak_$tag.c"
     68   local obj="$WORK/weak_$tag.o"
     69   printf '%s\n' "$WEAK_SRC" > "$src"
     70   "$KIT" cc -target "$triple" -O1 -ffreestanding -std=c11 -c "$src" \
     71     -o "$obj" > "$WORK/weak_$tag.cc.out" 2>&1
     72   "$KIT" objdump -d "$obj" > "$WORK/weak_$tag.dis" 2>&1
     73   local ncalls
     74   ncalls=$(sed -n '/<wcaller>:/,/^$/p' "$WORK/weak_$tag.dis" \
     75     | grep -cE "$call_mnemonics" || true)
     76   if [ "$ncalls" -eq 0 ]; then
     77     printf 'whole-program-inline FAILED: %s inlined a WEAK callee (interposition unsound)\n' \
     78       "$tag" >&2
     79     sed -n '/<wcaller>:/,/^$/p' "$WORK/weak_$tag.dis" | sed 's/^/  | /' >&2
     80     exit 1
     81   fi
     82   printf 'whole-program-inline %-8s weak callee kept out-of-line\n' "$tag"
     83 }
     84 check_weak_not_inlined aarch64-linux-gnu aa64
     85 check_weak_not_inlined x86_64-linux-gnu  x64
     86 check_weak_not_inlined riscv64-linux-gnu rv64
     87 
     88 # Metric: the whole-program inliner must actually fire at -O1 (not just the
     89 # streaming tiny-inliner, which emits opt.tiny_inline.inlined instead).
     90 read -r -d '' RUN_SRC <<'EOF' || true
     91 static int add1(int x) { return x + 1; }
     92 int main(void) { return add1(41) == 42 ? 0 : 1; }
     93 EOF
     94 printf '%s\n' "$RUN_SRC" > "$WORK/run.c"
     95 if ! KIT_METRICS=1 "$KIT" run --time -O1 "$WORK/run.c" \
     96     >"$WORK/run.out" 2>"$WORK/run.err"; then
     97   printf 'whole-program-inline FAILED: `kit run -O1` did not exit 0\n' >&2
     98   sed 's/^/  | /' "$WORK/run.err" >&2
     99   exit 1
    100 fi
    101 if ! grep -q 'opt.inline.inlined' "$WORK/run.err"; then
    102   printf 'whole-program-inline FAILED: opt.inline.inlined metric absent at -O1\n' >&2
    103   sed -n '1,80p' "$WORK/run.err" >&2
    104   exit 1
    105 fi
    106 printf 'whole-program-inline run     fired opt.inline.inlined, exit 0\n'
    107 
    108 # The kit-native build verbs (build-exe/build-lib/build-obj) compile through the
    109 # same kit_cg path as cc, so whole-program optimization participates without any
    110 # build-verb-specific wiring. Guard that: build-obj at -O1 must fuse, and
    111 # build-exe must produce a correct, fused executable.
    112 printf '%s\n' "$SRC" > "$WORK/verb.c"
    113 "$KIT" build-obj -O1 -ffreestanding "$WORK/verb.c" -o "$WORK/verb.o" \
    114   > "$WORK/verb.cc.out" 2>&1
    115 "$KIT" objdump -d "$WORK/verb.o" > "$WORK/verb.dis" 2>&1
    116 vcalls=$(sed -n '/<compute>:/,/^$/p' "$WORK/verb.dis" \
    117   | grep -cE "$call_mnemonics" || true)
    118 if [ "$vcalls" -ne 0 ]; then
    119   printf 'whole-program-inline FAILED: build-obj -O1 did not fuse (LTO bypassed)\n' >&2
    120   sed -n '/<compute>:/,/^$/p' "$WORK/verb.dis" | sed 's/^/  | /' >&2
    121   exit 1
    122 fi
    123 printf 'whole-program-inline build-obj fused (verb participates in LTO)\n'
    124 
    125 read -r -d '' VERB_EXE_SRC <<'EOF' || true
    126 static int add1(int x) { return x + 1; }
    127 static int twice(int x) { return add1(add1(x)); }
    128 int main(void) { return (twice(20) + add1(1)) == 24 ? 0 : 1; }
    129 EOF
    130 printf '%s\n' "$VERB_EXE_SRC" > "$WORK/verb_exe.c"
    131 if ! "$KIT" build-exe -O1 "$WORK/verb_exe.c" -o "$WORK/verb_exe" \
    132      > "$WORK/verb_exe.cc.out" 2>&1 || ! "$WORK/verb_exe"; then
    133   printf 'whole-program-inline FAILED: build-exe -O1 produced wrong result\n' >&2
    134   sed 's/^/  | /' "$WORK/verb_exe.cc.out" >&2
    135   exit 1
    136 fi
    137 printf 'whole-program-inline build-exe correct + fused\n'
    138 
    139 # Inline-policy hints: a `static inline` (HINT) callee raises the inliner's cost
    140 # budget above a plain static (DEFAULT); always_inline ignores the cap, noinline
    141 # blocks. The inline decision runs on target-independent IR, so the verdict is
    142 # identical on every arch. A ~30-op callee sits above the DEFAULT cap (20) and
    143 # below the HINT cap (40): the static-inline one fuses while the byte-identical
    144 # plain-static one does not. Regression guard for the keyword `inline` reaching
    145 # the codegen inline policy (it used to be dropped, so static inline silently
    146 # behaved like a plain static).
    147 mul() { local n=$1 s="x" i; for ((i = 1; i < n; i++)); do s="$s*x"; done; printf '%s' "$s"; }
    148 H30=$(mul 31)
    149 A60=$(mul 61)
    150 read -r -d '' HINT_SRC <<EOF || true
    151 static inline int ih(int x){ return $H30; }
    152 static        int id(int x){ return $H30; }
    153 __attribute__((always_inline)) static int ia(int x){ return $A60; }
    154 __attribute__((noinline))      static int iv(int x){ return x + 1; }
    155 int use_ih(int x){ return ih(x); }
    156 int use_id(int x){ return id(x); }
    157 int use_ia(int x){ return ia(x); }
    158 int use_iv(int x){ return iv(x); }
    159 EOF
    160 check_inline_hints() {
    161   local triple=$1
    162   local tag=$2
    163   local src="$WORK/hints_$tag.c"
    164   local obj="$WORK/hints_$tag.o"
    165   local dis="$WORK/hints_$tag.dis"
    166   printf '%s\n' "$HINT_SRC" > "$src"
    167   "$KIT" cc -target "$triple" -O1 -ffreestanding -std=c11 -c "$src" -o "$obj" \
    168     > "$WORK/hints_$tag.cc.out" 2>&1
    169   "$KIT" objdump -d "$obj" > "$dis" 2>&1
    170   # Isolate one function body: from its `<name>:` header to the next symbol
    171   # header (the disassembly has no blank separators, so a /^$/ window would run
    172   # to EOF and bleed across functions).
    173   calls_in() {
    174     awk -v f="<$1>:" '$0 ~ f {g = 1; next} /<[A-Za-z_].*>:/ {g = 0} g' "$dis" \
    175       | grep -cE "$call_mnemonics" || true
    176   }
    177   local ch cd ca cv
    178   ch=$(calls_in use_ih)
    179   cd=$(calls_in use_id)
    180   ca=$(calls_in use_ia)
    181   cv=$(calls_in use_iv)
    182   if [ "$ch" -ne 0 ]; then
    183     printf 'inline-hints FAILED: %s static-inline (HINT) callee not fused (%s call[s])\n' "$tag" "$ch" >&2
    184     exit 1
    185   fi
    186   if [ "$cd" -eq 0 ]; then
    187     printf 'inline-hints FAILED: %s plain-static (DEFAULT) callee fused above its budget\n' "$tag" >&2
    188     exit 1
    189   fi
    190   if [ "$ca" -ne 0 ]; then
    191     printf 'inline-hints FAILED: %s always_inline callee not fused\n' "$tag" >&2
    192     exit 1
    193   fi
    194   if [ "$cv" -eq 0 ]; then
    195     printf 'inline-hints FAILED: %s noinline callee was inlined\n' "$tag" >&2
    196     exit 1
    197   fi
    198   printf 'inline-hints %-8s HINT+ALWAYS fused, DEFAULT+NEVER kept out-of-line\n' "$tag"
    199 }
    200 check_inline_hints aarch64-linux-gnu aa64
    201 check_inline_hints x86_64-linux-gnu  x64
    202 check_inline_hints riscv64-linux-gnu rv64
    203 
    204 printf 'whole-program-inline: ok\n'