Skip to content

Instantly share code, notes, and snippets.

@travisdowns
Created July 16, 2026 07:16
Show Gist options
  • Select an option

  • Save travisdowns/8f448811c49f7388e64ca5525443525b to your computer and use it in GitHub Desktop.

Select an option

Save travisdowns/8f448811c49f7388e64ca5525443525b to your computer and use it in GitHub Desktop.
static vs static inline: inlining behavior sweep for clang-20 / gcc-13 (seastar PR 3519 discussion)

inline is still an optimization hint (clang-20 demo)

Empirical demo for the claim in scylladb/seastar#3519 (comment): in modern compilers static inline is a materially stronger hint to inline than plain static — the keyword is not just linkage/ODR relief.

Result

With clang-20 at -O2, the same function body is:

static static inline
remark not inlined (cost=405 > threshold=337) inlined (cost=405 < threshold=487)
asm 2 call sites + standalone body fully inlined, body deleted
$ ./demo.sh
== inliner remarks, plain static:
example.cpp:62:12: remark: '_ZL7computei' not inlined into '_Z7caller1i' because too costly to inline (cost=405, threshold=337)
example.cpp:66:12: remark: '_ZL7computei' not inlined into '_Z7caller2i' because too costly to inline (cost=405, threshold=337)
== inliner remarks, static inline (-DUSE_INLINE):
example.cpp:62:12: remark: '_ZL7computei' inlined into '_Z7caller1i' with (cost=405, threshold=487)
example.cpp:66:12: remark: '_ZL7computei' inlined into '_Z7caller2i' with (cost=-14595, threshold=487)
== asm evidence:
  static.s: 2 call site(s) to compute, 1 standalone definition(s)
  inline.s: 0 call site(s) to compute, 0 standalone definition(s)

Mechanism

The inline keyword makes clang attach the inlinehint attribute to the LLVM function, which selects a higher inline-cost threshold: 225 (-inline-threshold) for plain functions vs 325 (-inlinehint-threshold) at -O2/-O3. Both get a 1.5× bonus here because the callee is a single basic block, giving the observed 337 vs 487. Any function whose inline cost lands between the two thresholds is inlined only when declared inline.

The sweet spot (size sweep)

sweep.py generates a compute() of N statements (each ≈ cost 15: xor, mul, add) and probes both qualifiers:

compiler divergence window (N statements) cost range
clang-20 -O2 26 – 35 345 – 480
g++-13 -O2 11 – 24 n/a (different cost model)

Below the window both variants inline; above it neither does. example.cpp uses N=30 (cost 405), mid-window for clang. Note the windows don't overlap, so this exact file shows no divergence on gcc — regenerate with N≈15 for that.

Gotchas that will eat your repro

  • Two call sites minimum. A static (local) function with a single call site gets LLVM's last-call-to-static bonus (15000): deleting the function afterwards pays for inlining, so it inlines at practically any size. You can see the bonus fire in the second static inline remark above: after site 1 is inlined, site 2 becomes the last call and its cost drops to -14595.
  • Tail calls aren't call. return compute(a); compiles to jmp _ZL7computei, so grepping asm for call alone reports false "inlined". The callers add + 1 after the call to force a real call, and detection matches call|jmp.

Files

  • example.cpp — standalone repro (godbolt-friendly), -DUSE_INLINE toggles the qualifier
  • demo.sh — compiles both variants, shows remarks + asm evidence
  • sweep.py — the size sweep (python3 sweep.py [lo hi], CXX=/OPT= env overrides)
#!/bin/bash
# Compile example.cpp both ways and show that `static` stays a call while
# `static inline` is inlined.
set -euo pipefail
cd "$(dirname "$0")"
CXX=${CXX:-/usr/bin/clang++-20}
OPT=${OPT:--O2}
echo "== compiler: $($CXX --version | head -1), flags: $OPT"
echo
echo "== inliner remarks, plain static:"
$CXX $OPT -c -o /dev/null example.cpp -Rpass=inline -Rpass-missed=inline 2>&1 | grep compute
echo
echo "== inliner remarks, static inline (-DUSE_INLINE):"
$CXX $OPT -c -o /dev/null example.cpp -DUSE_INLINE -Rpass=inline -Rpass-missed=inline 2>&1 | grep compute
echo
$CXX $OPT -S -o static.s example.cpp
$CXX $OPT -S -o inline.s example.cpp -DUSE_INLINE
echo "== asm evidence (call/jmp to compute, and whether a standalone body exists):"
for f in static.s inline.s; do
calls=$(grep -cE '(call|jmp).*_ZL7computei' "$f" || true)
body=$(grep -c '^_ZL7computei:' "$f" || true)
echo " $f: $calls call site(s) to compute, $body standalone definition(s)"
done
// Demonstrates that `inline` is still an optimization *hint* in clang, not
// just an ODR/linkage matter: this function is inlined when declared
// `static inline` but stays a call when declared plain `static`.
//
// Mechanism: the `inline` keyword adds the `inlinehint` LLVM attribute, which
// raises the inline-cost threshold from 225 (-inline-threshold) to 325
// (-inlinehint-threshold) at -O2. compute() below has inline cost 405, and
// both thresholds get a 1.5x single-basic-block bonus (337 vs 487), so the
// cost lands between them: too big for `static`, small enough for
// `static inline`. Two call sites are required -- with only one, LLVM's
// last-call-to-static bonus (deleting the function pays for inlining it)
// makes it inline at any size.
//
// Try it:
// clang++-20 -O2 -S -o static.s example.cpp
// clang++-20 -O2 -S -o inline.s example.cpp -DUSE_INLINE
// grep -c '_ZL7computei' static.s inline.s
// or watch the cost model decide:
// clang++-20 -O2 -c -o /dev/null example.cpp -Rpass=inline -Rpass-missed=inline
#ifdef USE_INLINE
#define QUAL static inline
#else
#define QUAL static
#endif
QUAL int compute(int x) {
x += (x ^ 0) * 3;
x += (x ^ 31153) * 5;
x += (x ^ 62306) * 7;
x += (x ^ 27923) * 9;
x += (x ^ 59076) * 11;
x += (x ^ 24693) * 13;
x += (x ^ 55846) * 15;
x += (x ^ 21463) * 17;
x += (x ^ 52616) * 19;
x += (x ^ 18233) * 21;
x += (x ^ 49386) * 23;
x += (x ^ 15003) * 25;
x += (x ^ 46156) * 27;
x += (x ^ 11773) * 29;
x += (x ^ 42926) * 31;
x += (x ^ 8543) * 33;
x += (x ^ 39696) * 35;
x += (x ^ 5313) * 37;
x += (x ^ 36466) * 39;
x += (x ^ 2083) * 41;
x += (x ^ 33236) * 43;
x += (x ^ 64389) * 45;
x += (x ^ 30006) * 47;
x += (x ^ 61159) * 49;
x += (x ^ 26776) * 51;
x += (x ^ 57929) * 53;
x += (x ^ 23546) * 55;
x += (x ^ 54699) * 57;
x += (x ^ 20316) * 59;
x += (x ^ 51469) * 61;
return x;
}
int caller1(int a) {
return compute(a) + 1;
}
int caller2(int a) {
return compute(a * 2 + 1) + 2;
}
#!/usr/bin/env python3
"""Sweep function sizes to find where `static inline` is inlined but `static` isn't.
For each size N, generates a compute() function with N arithmetic statements and
two external call sites (two call sites matter: a single-call-site static gets
LLVM's last-call-to-static bonus and is always inlined). Compiles with both
`static` and `static inline`, then checks the asm for surviving calls and parses
the inline cost/threshold from -Rpass remarks.
"""
import re
import subprocess
import sys
import tempfile
import os
CXX = os.environ.get("CXX", "/usr/bin/clang++-20")
OPT = os.environ.get("OPT", "-O2")
TEMPLATE = """\
{qual} int compute(int x) {{
{body}
return x;
}}
int caller1(int a) {{
return compute(a) + 1;
}}
int caller2(int a) {{
return compute(a * 2 + 1) + 2;
}}
"""
def gen_body(n: int) -> str:
lines = []
for i in range(n):
c = (i * 2654435761) & 0xFFFF
lines.append(f" x += (x ^ {c}) * {2 * i + 3};")
return "\n".join(lines)
def probe(n: int, qual: str, workdir: str):
src = os.path.join(workdir, "test.cpp")
asm = os.path.join(workdir, "test.s")
with open(src, "w") as f:
f.write(TEMPLATE.format(qual=qual, body=gen_body(n)))
r = subprocess.run(
[CXX, OPT, "-S", "-o", asm, src,
"-Rpass=inline", "-Rpass-missed=inline"],
capture_output=True, text=True)
if r.returncode != 0:
sys.exit(f"compile failed: {r.stderr}")
with open(asm) as f:
calls = len(re.findall(r"(?:call|jmp).*compute", f.read()))
# remark examples:
# remark: 'compute' inlined into 'caller1' with (cost=310, threshold=325)
# remark: 'compute' not inlined into 'caller1' because too costly to inline (cost=330, threshold=325)
m = re.search(r"\(cost=(-?\d+), threshold=(\d+)\)", r.stderr)
cost, thresh = (int(m.group(1)), int(m.group(2))) if m else (None, None)
return calls, cost, thresh
def main():
lo, hi = 1, 40
if len(sys.argv) == 3:
lo, hi = int(sys.argv[1]), int(sys.argv[2])
print(f"compiler={CXX} {OPT}")
print(f"{'N':>3} | {'static':^22} | {'static inline':^22} | divergent")
print(f"{'':>3} | {'calls':>5} {'cost':>6} {'thresh':>6} | {'calls':>5} {'cost':>6} {'thresh':>6} |")
with tempfile.TemporaryDirectory() as wd:
for n in range(lo, hi + 1):
s_calls, s_cost, s_thresh = probe(n, "static", wd)
i_calls, i_cost, i_thresh = probe(n, "static inline", wd)
marker = " <=== static: call, inline: inlined" if s_calls > 0 and i_calls == 0 else ""
fmt = lambda v: "?" if v is None else v
print(f"{n:>3} | {s_calls:>5} {fmt(s_cost):>6} {fmt(s_thresh):>6} "
f"| {i_calls:>5} {fmt(i_cost):>6} {fmt(i_thresh):>6} |{marker}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment