What Whole-Module Optimization Actually Deletes
You write protocols and generics for the humans; the release binary runs like hand-written C. Here's a stage-by-stage look at what the optimizer removes on the way - specialization, devirtualization, inlining, ARC - and why the abstraction you wrote never reaches the CPU.
In the compiler guardrails post I claimed that the abstraction you write for the humans never makes it to the CPU - that swiftc melts your protocols and generics into something a C programmer in a hurry would recognize. A few people asked what that melting actually looks like, step by step. Fair question, because the answer is one of the most satisfying things the toolchain does, and it happens invisibly on every archive build.
So let’s take one small, ordinary function and watch the optimizer take it apart.
The specimen
func total<S: Sequence>(_ xs: S) -> Double where S.Element == Double {
var sum = 0.0
for x in xs { sum += x }
return sum
}
let sum = total(prices) // prices: [Double]
Nice and abstract: any sequence of doubles, one implementation. Exactly the kind of code Swift wants you to write - and, naively compiled, exactly the kind of code that should be slow. Every iteration goes through a protocol witness table to find next(), every element arrives behind a layer of generic indirection, and the loop can’t be reasoned about because the compiler doesn’t know what S is.
Unless it does. Step through what each optimization pass removes:
Why “whole-module” is the load-bearing word
Every pass in that sequence depends on the compiler seeing both sides of the call: the generic function and the concrete call site. Compile them in isolation - one file at a time, the old default - and total has to be compiled as the fully-generic, witness-table-driven version, because some unknown caller somewhere might pass it a linked list.
Whole-module optimization is just the permission slip: with the entire module visible at once, the compiler knows every caller, sees that S is only ever [Double], and can commit to the specialized version - then let devirtualization, inlining, and ARC elimination cascade from there, each pass exposing the next. Cross-module optimization and @inlinable extend the same visibility across package boundaries, which is a big part of why SPM-heavy codebases care about their settings here.
The part worth internalizing
The stage-4 loop - pointer, stride, add, no calls, no reference counting - is what you’d have written in C if you were optimizing by hand. The difference is that your version states its intent (any sequence of doubles, summed), gets checked by the type system, and still compiles down to that loop.
That’s the deal I keep coming back to: abstraction in Swift is a source-code phenomenon. You pay for it in compile time, occasionally in binary size - and almost never, in a release build, at runtime. Which means the common performance instinct of “avoid protocols, avoid generics, they’re slow” is optimizing against a cost model the optimizer deleted years ago. Write the clear version; the compiler’s half of the bargain is better than yours.