How does the inliner decide, and what do @inline(never), @inline(__always) and @inlinable do?
Predict first
What happens to each call in optimised SIL? (caller1 calls small; caller2 calls the @inline(never) function.)
func small(_ x: Int) -> Int { x &* 3 &+ 1 }
@inline(never) func neverInlined(_ x: Int) -> Int { x &* 3 &+ 1 }
@inline(never) func caller1(_ x: Int) -> Int { small(x) }
@inline(never) func caller2(_ x: Int) -> Int { neverInlined(x) }
print(caller1(2), caller2(2))
- Both stay calls
- small is inlined, neverInlined stays a call
- Both are inlined
Mental model. Inlining is copying a recipe step into your own recipe card instead of saying “see the other book”. It saves flipping pages, and it makes your card longer.
The SIL inliner replaces a call with the callee’s body when its cost model says that is a win: small bodies, a body that becomes simpler once its arguments are known, or a callee used once. It avoids growth in code size where gains are unlikely. Inlining is itself an enabler: once the body is in place, constants fold, retains and releases cancel and further calls become direct.
You can steer it. @inline(never) forbids inlining (useful for profiling, keeping a function visible, or limiting code growth). @inline(__always) asks for it regardless of cost, which is a blunt tool that can bloat code. Across a module boundary the callee’s body is not visible at all unless it is @inlinable (or the call is within one WMO module), which is how a library chooses to expose its implementation for speed at the cost of binding clients to it.
The small function’s body is copied into caller1, so no call remains. The @inline(never) one is still called.
Common traps
- Sprinkling
@inline(__always)everywhere. Code grows, instruction caches suffer, and the compiler usually made the right choice. - Expecting cross-module inlining without
@inlinable. The body is not there to inline.
They might follow up with
- How do you confirm a call was inlined? Read the optimised SIL: no
function_refto the callee, and its operations appear in the caller. - What is
@_transparent? An underscored attribute that forces inlining even in-Onone, used mainly inside the standard library.
Related questions
- 5.2.1 What is devirtualisation, and why does -O alone not remove a class method’s vtable lookup?
- 5.4.2 What is a .swiftinterface file, and what does it show about @inlinable and @frozen?
- 1.7.4 What are @usableFromInline and @inlinable in relation to access control?
← What does whole-module optimisation change, and what does it cost? What is exclusivity enforcement, and what does it cost at runtime? →