kit

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

commit 965e6ceb27de27a316c8a251786d8717317ff6e0
parent a66c55af01ab541d4e379ef57d1ab8becdcf26b3
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 15 Jun 2026 19:13:20 -0700

opt: make the O1 whole-program inliner linear; fix static-inline hint

The -O1 whole-program inliner was superlinear (~O(n^2.5-3)): 9.6s / 86% of an
11.2s sqlite compile, exploding to 332x -O0 at 1600 funcs. Its hot gates used
linear scans -- funcset_find/funcset_index walked all funcs per call, and
recursive_or_scc -> func_reaches was a per-candidate reachability DFS paying that
O(n) lookup at every hop.

Attach an InlineIndex to the FuncSet: a symbol->index hash map (O(1)
funcset_find) plus per-function SCC ids from one iterative Tarjan pass. The
recursion gate is only ever asked where a caller->callee edge exists, where
"callee reaches caller" is exactly "same SCC", so it becomes an
scc[caller]==scc[callee] compare. Verdicts are identical to the old DFS -- the
sqlite -O1 object is byte-for-byte unchanged (same 730 inlines) -- only the
asymptotics change. opt.inline.total 9.6s -> 0.24s; -O1 sqlite 11.2s -> 1.6s
(~10x -O0, was ~70x); the synthetic call-graph sweep is now linear (doubling
funcs ~doubles -O1 time). The streaming tiny-inliner's ad-hoc 2-element sets
leave FuncSet.index NULL and keep the direct linear/DFS fallback.

Inline hints: the C "inline" keyword is tracked on the specifier flags, separate
from the __attribute__ flags decl building copied in, so "static inline" silently
fell back to the DEFAULT inline policy (cost cap 20) instead of HINT (cap 40) --
only always_inline/noinline (attributes) reached codegen. Merge specs DF_INLINE
onto the decl so static-inline bodies inline at sizes a plain static would not.
always_inline (unbounded) and noinline (never) already worked.

doc/OPT.md sec.8 updated to the fixed numbers + the hint table;
test/opt/whole_program_inline.sh gains a 4-policy (HINT/ALWAYS/DEFAULT/NEVER)
regression check on all three arches.

Gates: -O0 sqlite byte-identical; -O1 sqlite byte-identical to pre-perf-change +
links/runs (sum=42); ASan+verifiers clean; test-parse/cg-api/opt/pp/toy/smoke-x64
/smoke-rv64/isa all green.

Diffstat:
Mdoc/OPT.md | 116++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mlang/c/parse/parse.c | 9+++++++++
Msrc/opt/opt_internal.h | 6++++++
Msrc/opt/pass_inline.c | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/opt/whole_program_inline.sh | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 305 insertions(+), 48 deletions(-)

diff --git a/doc/OPT.md b/doc/OPT.md @@ -381,7 +381,13 @@ transform or analysis; the file paths orient the reader. lazily re-lowered callee cache), gates on a tiny straightline-cost cap and a whitelist that excludes calls/control-rich constructs, refuses self/recursive callees, and splices the cloned body in. The whole-program inliner machinery - (`inline_call_site` and its gates) also lives here. + (`opt_inline`, `inline_call_site`, and the cost/growth/policy gates) also lives + here; it builds an `InlineIndex` over the `FuncSet` once — a symbol→index hash + map plus per-function SCC ids (iterative Tarjan) — so callee resolution and the + recursion gate (`recursive_or_scc`, an `scc[caller] == scc[callee]` compare) + are O(1), keeping the pass linear in call sites (Section 8). Both inliners + honor the per-function/per-call `KitCgInlinePolicy` (DEFAULT/HINT/ALWAYS/NEVER), + which is how C `inline` / `always_inline` / `noinline` reach codegen. - **Address folding** (`src/opt/pass_addr_fold.c`): the always-on O1 HIR folds — `opt_addr_xform_pregs` (fold `ADDR_OF(local)` into direct `OPK_LOCAL` load/store operands and clear `FSF_ADDR_TAKEN` when all such defs retire), @@ -565,62 +571,76 @@ recording, *not* the finalize sweep that follows it. ``` $ KIT_METRICS=1 kit cc -O1 -c sqlite3.c --sysroot "$SDK" -o sqlite3.o kit metrics: - compile.tu 4.7M ticks (frontend: lex+pp+parse+CG recording) - opt.inline.total 230.6M ticks (whole-program inliner, 1 call) - opt.o1.total 25.7M ticks (per-function pipeline, summed over 2580 funcs) - opt.regalloc 11.5M ticks (biggest per-function pass) + compile.tu ~10M ticks (frontend: lex+pp+parse+CG recording) + opt.inline.total ~6M ticks (whole-program inliner, 1 call) + opt.o1.total ~38M ticks (per-function pipeline, summed over 2580 funcs) + opt.regalloc ~17M ticks (biggest per-function pass) ... opt.funcs=2580 opt.blocks=122208 opt.tiny_inline.inlined=13 opt.inline.inlined=730 ``` ### Measured cost and scaling (sqlite amalgamation, arm64/Darwin) -Compiling the ~263 K-line sqlite amalgamation (`-c`, release kit) the wall-clock -split is stark — `-O1` is **~70× slower than `-O0`** (11.2 s vs 0.16 s; tcc does -the whole file in 0.06 s): +Compiling the ~263 K-line sqlite amalgamation (`-c`, release kit), `-O1` runs in +**~1.6 s — about 10× `-O0`** (0.16 s; tcc does the whole file in 0.06 s). The +time is dominated by the per-function pipeline, with the linear-scan allocator +and live-range construction the largest line items — exactly the shape a no-SSA +`-O1` should have: | phase (24 MHz ticks → s) | seconds | share | |-------------------------------------|----------:|------:| -| frontend (`compile.tu`) | 0.20 | 2% | -| CG→opt lowering (`cg_ir_lower`) | 0.23 | 2% | -| **whole-program inliner** (`opt.inline.total`) | **9.61** | **86%** | -| per-function pipeline (`opt.o1.total`) | 1.07 | 10% | -| — of which regalloc + live-ranges | 0.64 | | - -The whole-program inliner dominates, and it is **superlinear**. A synthetic -sweep (N small functions in a dense call graph, best-of-3) shows the `-O1` time -exploding while `-O0` stays flat: - -| funcs | `-O0` | `-O1` | O1/O0 | -|------:|------:|------:|------:| -| 200 | 0.027 | 0.060 | 2.3× | -| 400 | 0.027 | 0.240 | 8.8× | -| 800 | 0.030 | 1.524 | 50.5× | -| 1600 | 0.035 | 11.48 | 332× | - -Doubling the function count multiplies `-O1` time by ~6–8× — an exponent near -2.5–3. The root cause is in `pass_inline.c`: the inliner runs up to four -fixpoint iterations over the whole `FuncSet`, and its hot gates resolve callees -and check for recursion/SCC membership with **linear scans** — `funcset_find` -and `funcset_index` walk all functions on every call, and `recursive_or_scc` → -`func_reaches` is a per-candidate DFS whose every hop pays that O(n) lookup. With -2580 functions and ~24 K candidate call sites this is the textbook -"superlinear axis = bug" trap (`doc/plan/PERF.md`). The payoff is also small: -730 of 24 K candidates inline. - -By contrast the per-function pipeline is well-behaved (~1 s total, linear in -function count); within it the linear-scan allocator + live-range construction -are the largest line items, as expected for a no-SSA `-O1`. - -**Implication for the "fast O1" goal.** The non-SSA per-function pipeline already -meets the bar — strip the inliner and `-O1` would be ~1.5 s on sqlite (linear, -~10× `-O0`). The whole-program inliner is what breaks it. The fixes are -self-contained and do not touch correctness: (1) index the `FuncSet` by symbol -(hash map) so `funcset_find`/`funcset_index` are O(1); (2) memoize the -reachability/SCC check instead of re-running `func_reaches` per candidate; and/or -(3) reconsider whether whole-program inlining belongs at `-O1` at all versus -behind `-O2`/`-flto`, leaving `-O1` with only the cheap streaming `try_tiny_inline` -(0.28 s on sqlite). Any of these restores the linear, "fast" `-O1` design point. +| frontend (`compile.tu`) | ~0.3 | ~20% | +| CG→opt lowering (`cg_ir_lower`) | ~0.4 | ~25% | +| whole-program inliner (`opt.inline.total`) | ~0.24 | ~15% | +| per-function pipeline (machinize→regalloc→emit) | ~0.7 | ~45% | +| — of which regalloc + live-ranges | ~0.6 | | + +The cost scales **linearly** in function count. A synthetic sweep (N small +functions in a dense call graph, best-of-3) — doubling N roughly doubles `-O1` +time, converging on the same slope as `-O0`: + +| funcs | `-O0` | `-O1` | O1/O0 | `-O1` ×/2× funcs | +|------:|------:|------:|------:|-----------------:| +| 400 | 0.027 | 0.038 | 1.4× | — | +| 800 | 0.030 | 0.049 | 1.6× | 1.3× | +| 1600 | 0.034 | 0.074 | 2.2× | 1.5× | +| 3200 | 0.043 | 0.131 | 3.1× | 1.8× | +| 6400 | 0.062 | 0.264 | 4.3× | 2.0× | + +**This was not always so.** The whole-program inliner used to be superlinear +(≈O(n^2.5–3): 9.6 s / 86 % of an 11.2 s `-O1` sqlite compile, exploding to 332× +`-O0` at 1600 functions). The hot gates resolved callees and checked recursion +with linear scans — `funcset_find`/`funcset_index` walked all functions on every +call, and `recursive_or_scc` → `func_reaches` was a per-candidate reachability +DFS whose every hop paid that O(n) lookup. The fix (in `pass_inline.c`) attaches +an `InlineIndex` to the `FuncSet`: a symbol→index hash map (O(1) `funcset_find`) +plus **per-function SCC ids computed once by iterative Tarjan**. The recursion +gate is then a single observation — the inliner only asks "is this call +recursive?" where a `caller→callee` edge exists, and there "callee reaches +caller" is *exactly* "caller and callee share an SCC", so it reduces to an +`scc[caller] == scc[callee]` compare. The verdicts are identical to the old DFS +(the sqlite object stayed **byte-for-byte unchanged**, same 730 inlines); only +the asymptotics changed. `opt.inline.total` dropped 9.6 s → ~0.24 s. + +### Inline hints + +The inliner honors the frontend's per-function `KitCgInlinePolicy`, which the C +frontend derives from declaration hints: + +| C hint | policy | inliner behavior | +|-------------------------------------|----------|------------------| +| (none) | DEFAULT | inline if cost ≤ 20 | +| `inline` / `static inline` | HINT | inline if cost ≤ 40 | +| `__attribute__((always_inline))` | ALWAYS | inline regardless of cost (recursion still blocks) | +| `__attribute__((noinline))` | NEVER | never inline | + +So a `static inline` body inlines at sizes a plain `static` one would not, and +`always_inline` ignores the budget entirely, while `noinline` is always honored. +(The `inline` *keyword* lives on the specifier flags, separate from the +`__attribute__` flags; it has to be merged onto the declaration so the policy +reaches codegen — a step that was missing, which had silently demoted every +`static inline` to DEFAULT until corrected.) `test/opt/whole_program_inline.sh` +guards all four policies on every arch. ### Code quality diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -1242,6 +1242,15 @@ static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty, decl_in.linkage == DL_EXTERNAL ? p->default_visibility : SV_DEFAULT; attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in); attr_list_to_decl(p->c, p->decls, dattrs, &decl_in); + /* The `inline` specifier is a keyword carried on specs->flags, separate from + * the __attribute__ flags attr_list_to_decl applies. It must be merged onto + * the decl so decl_inline_policy() sees it on both the recorded symbol attrs + * (decl_declare below) and the returned decl flags (out_decl_flags, used to + * build the function descriptor). Without this, `static inline` functions + * silently fell back to the DEFAULT inline policy — only the always_inline / + * noinline attributes reached codegen — so a `static inline` body got no + * stronger inlining than a plain static one. */ + decl_in.flags |= (specs->flags & DF_INLINE); did = decl_declare(p->decls, &decl_in); fsym = decl_obj_sym(p->decls, did); e = scope_define(p, fname, SEK_FUNC, fn_ty); diff --git a/src/opt/opt_internal.h b/src/opt/opt_internal.h @@ -23,6 +23,12 @@ struct FuncSet { Func** funcs; u32 nfuncs; u32 cap; + /* Opaque InlineIndex* (pass_inline.c): a symbol->index map plus per-function + * SCC ids, built once by opt_inline so callee resolution and the + * recursion/SCC gate are O(1) instead of linear scans over `funcs`. NULL for + * the ad-hoc 2-element sets the streaming tiny-inliner builds, which fall back + * to the direct linear scan / reachability DFS. */ + void* index; }; typedef struct OptBlockList { diff --git a/src/opt/pass_inline.c b/src/opt/pass_inline.c @@ -2,6 +2,7 @@ #include "core/arena.h" #include "core/core.h" +#include "core/hashmap.h" #include "core/metrics.h" #include "opt/opt_internal.h" @@ -75,8 +76,29 @@ static void instvec_push(InstVec* iv, const Inst* in) { iv->v[iv->n++] = *in; } +/* Symbol -> index-into-fs->funcs map. Key 0 (OBJ_SYM_NONE) is the empty-slot + * sentinel in the hashmap, which is fine: a NONE-named function is never a + * callee (no symbol to call) and is handled by the linear fallback as a + * caller. */ +HASHMAP_DEFINE(InlineSymMap, ObjSymId, u32, hash_u32); + +/* O(1)-lookup acceleration for opt_inline, attached to FuncSet.index. Built + * once over the whole-program set; the streaming tiny-inliner leaves + * FuncSet.index NULL and the helpers below fall back to a linear scan over its + * 2-element set. */ +typedef struct InlineIndex { + InlineSymMap sym2idx; /* ObjSymId -> index into fs->funcs */ + u32* scc; /* SCC id per fs index; same id <=> mutually recursive */ + u32 nfuncs; /* size of scc[] (== fs->nfuncs at build time) */ +} InlineIndex; + static Func* funcset_find(FuncSet* fs, ObjSymId sym) { if (!fs || sym == OBJ_SYM_NONE) return NULL; + if (fs->index) { + InlineIndex* idx = (InlineIndex*)fs->index; + u32* slot = InlineSymMap_get(&idx->sym2idx, sym); + return slot ? fs->funcs[*slot] : NULL; + } for (u32 i = 0; i < fs->nfuncs; ++i) if (fs->funcs[i] && fs->funcs[i]->name == sym) return fs->funcs[i]; return NULL; @@ -84,6 +106,13 @@ static Func* funcset_find(FuncSet* fs, ObjSymId sym) { static int funcset_index(FuncSet* fs, Func* f) { if (!fs || !f) return -1; + if (fs->index && f->name != OBJ_SYM_NONE) { + InlineIndex* idx = (InlineIndex*)fs->index; + u32* slot = InlineSymMap_get(&idx->sym2idx, f->name); + /* Guard against a (deduped-away in practice) name collision: only trust the + * mapped slot when it actually points back at f. */ + if (slot && fs->funcs[*slot] == f) return (int)*slot; + } for (u32 i = 0; i < fs->nfuncs; ++i) if (fs->funcs[i] == f) return (int)i; return -1; @@ -140,6 +169,21 @@ static int func_reaches(FuncSet* fs, Func* from, Func* target, u8* seen) { static int recursive_or_scc(FuncSet* fs, Func* caller, Func* callee) { if (caller == callee) return 1; + /* Whole-program path: the inliner only ever asks this where a caller->callee + * call edge exists, so "callee can transitively reach caller" is exactly + * "caller and callee share a strongly-connected component". With the SCC ids + * precomputed (inline_index_build) this is O(1) and gives the identical + * verdict the linear func_reaches DFS would. */ + if (fs->index) { + InlineIndex* idx = (InlineIndex*)fs->index; + if (idx->scc) { + int ci = funcset_index(fs, caller); + int ce = funcset_index(fs, callee); + if (ci >= 0 && ce >= 0 && (u32)ci < idx->nfuncs && (u32)ce < idx->nfuncs) + return idx->scc[ci] == idx->scc[ce]; + } + } + /* Ad-hoc 2-element set (streaming tiny-inliner): direct reachability DFS. */ u8* seen = arena_zarray(fs->arena, u8, fs->nfuncs ? fs->nfuncs : 1u); return func_reaches(fs, callee, caller, seen); } @@ -664,9 +708,121 @@ static void inline_order_visit(InlineOrderCtx* ctx, Func* f) { ctx->order[ctx->norder++] = f; } +/* Count the direct in-set callees of f (CSR degree / fill helper). */ +static u32 inline_collect_callees(FuncSet* fs, Func* f, u32* out, u32 base) { + u32 n = 0; + if (!f) return 0; + for (u32 b = 0; b < f->nblocks; ++b) { + Block* bl = &f->blocks[b]; + for (u32 k = 0; k < bl->ninsts; ++k) { + Func* c = direct_callee(fs, &bl->insts[k]); + int ci = c ? funcset_index(fs, c) : -1; + if (ci < 0) continue; + if (out) out[base + n] = (u32)ci; + ++n; + } + } + return n; +} + +/* Build FuncSet.index: a symbol->index map and per-function SCC ids over the + * direct-call graph, so funcset_find and the recursion/SCC gate are O(1). The + * SCC partition is invariant under the inlining the pass performs (it only adds + * caller->X edges where X already sat strictly below the caller in the + * condensation DAG, and only removes cross-SCC call edges), so one build serves + * every fixpoint iteration. Tarjan's algorithm, iterative to bound stack depth + * on deep call chains. O(V + E). */ +static void inline_index_build(FuncSet* fs, InlineIndex* idx) { + u32 n = fs->nfuncs; + u32 *deg, *off, *edges, *comp, *st_node, *st_ei, *low; + i32* dfsnum; + u8* onstack; + u32 nedges, comp_top = 0, st_top = 0, counter = 0, ncomp = 0; + memset(idx, 0, sizeof *idx); + InlineSymMap_init_cap(&idx->sym2idx, fs->c->ctx->heap, 0); + for (u32 i = 0; i < n; ++i) + if (fs->funcs[i] && fs->funcs[i]->name != OBJ_SYM_NONE) + (void)InlineSymMap_set(&idx->sym2idx, fs->funcs[i]->name, i); + fs->index = idx; /* enables the O(1) funcset_find/index used just below */ + idx->nfuncs = n; + if (n == 0) return; + + deg = arena_zarray(fs->arena, u32, n); + for (u32 i = 0; i < n; ++i) deg[i] = inline_collect_callees(fs, fs->funcs[i], NULL, 0); + off = arena_array(fs->arena, u32, n + 1u); + off[0] = 0; + for (u32 i = 0; i < n; ++i) off[i + 1u] = off[i] + deg[i]; + nedges = off[n]; + edges = arena_array(fs->arena, u32, nedges ? nedges : 1u); + for (u32 i = 0; i < n; ++i) + (void)inline_collect_callees(fs, fs->funcs[i], edges, off[i]); + + idx->scc = arena_array(fs->arena, u32, n); + dfsnum = arena_array(fs->arena, i32, n); + low = arena_array(fs->arena, u32, n); + onstack = arena_zarray(fs->arena, u8, n); + comp = arena_array(fs->arena, u32, n); + st_node = arena_array(fs->arena, u32, n); + st_ei = arena_array(fs->arena, u32, n); + for (u32 i = 0; i < n; ++i) { + dfsnum[i] = -1; + idx->scc[i] = 0; + } + for (u32 root = 0; root < n; ++root) { + if (dfsnum[root] != -1) continue; + st_node[st_top] = root; + st_ei[st_top] = 0; + ++st_top; + while (st_top) { + u32 v = st_node[st_top - 1u]; + u32 ei = st_ei[st_top - 1u]; + if (ei == 0) { /* pre-order visit */ + dfsnum[v] = (i32)counter; + low[v] = counter; + ++counter; + comp[comp_top++] = v; + onstack[v] = 1; + } + if (ei < deg[v]) { + u32 w = edges[off[v] + ei]; + st_ei[st_top - 1u] = ei + 1u; + if (dfsnum[w] == -1) { /* tree edge: descend */ + st_node[st_top] = w; + st_ei[st_top] = 0; + ++st_top; + } else if (onstack[w] && (u32)dfsnum[w] < low[v]) { + low[v] = (u32)dfsnum[w]; /* back/cross edge to a live ancestor */ + } + } else { /* post-order: v's subtree done */ + if (low[v] == (u32)dfsnum[v]) { + for (;;) { + u32 w = comp[--comp_top]; + onstack[w] = 0; + idx->scc[w] = ncomp; + if (w == v) break; + } + ++ncomp; + } + --st_top; + if (st_top) { + u32 p = st_node[st_top - 1u]; + if (low[v] < low[p]) low[p] = low[v]; + } + } + } + } +} + +static void inline_index_fini(FuncSet* fs, InlineIndex* idx) { + InlineSymMap_fini(&idx->sym2idx); + fs->index = NULL; +} + void opt_inline(FuncSet* fs, int max_iters) { if (!fs || fs->nfuncs == 0 || max_iters <= 0) return; if (max_iters > 4) max_iters = 4; + InlineIndex idx; + inline_index_build(fs, &idx); u32* base_cost = arena_array(fs->arena, u32, fs->nfuncs); for (u32 i = 0; i < fs->nfuncs; ++i) base_cost[i] = func_inline_cost(fs->funcs[i]); @@ -697,6 +853,7 @@ void opt_inline(FuncSet* fs, int max_iters) { } if (!changed) break; } + inline_index_fini(fs, &idx); } /* Streaming single-caller variant for the O1 pipeline. Same gates as diff --git a/test/opt/whole_program_inline.sh b/test/opt/whole_program_inline.sh @@ -135,4 +135,69 @@ if ! "$KIT" build-exe -O1 "$WORK/verb_exe.c" -o "$WORK/verb_exe" \ fi printf 'whole-program-inline build-exe correct + fused\n' +# Inline-policy hints: a `static inline` (HINT) callee raises the inliner's cost +# budget above a plain static (DEFAULT); always_inline ignores the cap, noinline +# blocks. The inline decision runs on target-independent IR, so the verdict is +# identical on every arch. A ~30-op callee sits above the DEFAULT cap (20) and +# below the HINT cap (40): the static-inline one fuses while the byte-identical +# plain-static one does not. Regression guard for the keyword `inline` reaching +# the codegen inline policy (it used to be dropped, so static inline silently +# behaved like a plain static). +mul() { local n=$1 s="x" i; for ((i = 1; i < n; i++)); do s="$s*x"; done; printf '%s' "$s"; } +H30=$(mul 31) +A60=$(mul 61) +read -r -d '' HINT_SRC <<EOF || true +static inline int ih(int x){ return $H30; } +static int id(int x){ return $H30; } +__attribute__((always_inline)) static int ia(int x){ return $A60; } +__attribute__((noinline)) static int iv(int x){ return x + 1; } +int use_ih(int x){ return ih(x); } +int use_id(int x){ return id(x); } +int use_ia(int x){ return ia(x); } +int use_iv(int x){ return iv(x); } +EOF +check_inline_hints() { + local triple=$1 + local tag=$2 + local src="$WORK/hints_$tag.c" + local obj="$WORK/hints_$tag.o" + local dis="$WORK/hints_$tag.dis" + printf '%s\n' "$HINT_SRC" > "$src" + "$KIT" cc -target "$triple" -O1 -ffreestanding -std=c11 -c "$src" -o "$obj" \ + > "$WORK/hints_$tag.cc.out" 2>&1 + "$KIT" objdump -d "$obj" > "$dis" 2>&1 + # Isolate one function body: from its `<name>:` header to the next symbol + # header (the disassembly has no blank separators, so a /^$/ window would run + # to EOF and bleed across functions). + calls_in() { + awk -v f="<$1>:" '$0 ~ f {g = 1; next} /<[A-Za-z_].*>:/ {g = 0} g' "$dis" \ + | grep -cE "$call_mnemonics" || true + } + local ch cd ca cv + ch=$(calls_in use_ih) + cd=$(calls_in use_id) + ca=$(calls_in use_ia) + cv=$(calls_in use_iv) + if [ "$ch" -ne 0 ]; then + printf 'inline-hints FAILED: %s static-inline (HINT) callee not fused (%s call[s])\n' "$tag" "$ch" >&2 + exit 1 + fi + if [ "$cd" -eq 0 ]; then + printf 'inline-hints FAILED: %s plain-static (DEFAULT) callee fused above its budget\n' "$tag" >&2 + exit 1 + fi + if [ "$ca" -ne 0 ]; then + printf 'inline-hints FAILED: %s always_inline callee not fused\n' "$tag" >&2 + exit 1 + fi + if [ "$cv" -eq 0 ]; then + printf 'inline-hints FAILED: %s noinline callee was inlined\n' "$tag" >&2 + exit 1 + fi + printf 'inline-hints %-8s HINT+ALWAYS fused, DEFAULT+NEVER kept out-of-line\n' "$tag" +} +check_inline_hints aarch64-linux-gnu aa64 +check_inline_hints x86_64-linux-gnu x64 +check_inline_hints riscv64-linux-gnu rv64 + printf 'whole-program-inline: ok\n'