What does whole-module optimisation change, and what does it cost?
Predict first
A module takes 10 minutes to build in Release after a one-line change. What is the most likely reason?
// A: WMO recompiles the whole module for any change
// B: the optimiser is broken
// C: bitcode
- A
- B
- C
Mental model. A per-file build is proofreading each chapter alone. WMO is reading the whole book, which catches more, and means any edit sends you back to page one.
Normally the compiler works per file (often in parallel), knowing about other files only through their declarations. WMO (-wmo, the Release default in Xcode and SwiftPM) compiles the module as one unit. That gives the optimiser every function body and every type: it can inline across files, specialise generics defined in other files, devirtualise internal classes, remove dead code and unused conformances, and shrink the binary.
The price is build time and incrementality. Changing one file means recompiling the whole module, and a module compiles in fewer parallel jobs. That is why Debug uses incremental builds and Release uses WMO. It also means a huge module can be a build-time problem: splitting into several smaller modules restores parallelism, but then optimisation stops at the module boundary (no cross-module inlining or specialisation unless the code is @inlinable). The trade-off is between runtime speed inside a module and build speed across modules.
Whole-module builds cannot be incremental, so every change recompiles all files together. Splitting the module or using faster configurations for development helps.
Common traps
- Turning WMO on in Debug to “test performance”. You lose incremental builds and slow every edit.
- Splitting a hot path across modules and expecting it to inline. Optimisation stops at the module boundary unless the code is
@inlinable.
They might follow up with
- Where is WMO set? Build setting Compilation Mode in Xcode (Whole Module for Release); SwiftPM release builds use it automatically.
- How is it different from LTO? LTO optimises across object files at the LLVM level, after Swift’s own SIL optimisations have run.
Related questions
- 5.2.1 What is devirtualisation, and why does -O alone not remove a class method’s vtable lookup?
- 5.6.2 How does incremental compilation decide what to rebuild, and why does a small edit sometimes rebuild half the…
- 1.11.2 Why does final (and whole-module optimization) help performance?
← What is devirtualisation, and why does -O alone not remove a class… How does the inliner decide, and what do @inline(never)… →