How does the inliner decide, and what do @inline(never), @inline(__always) and @inlinable do?

5.2.3 · L5 · Interview

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_ref to 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? →