What do -Onone, -O, -Osize and -Ounchecked each change, and which safety checks survive in a release build?
Predict first
Does the SIL of add contain an overflow check at -O?
import Swift
@inline(never) func add(_ a: Int, _ b: Int) -> Int { a + b }
- Yes: cond_fail "arithmetic overflow"
- No, release builds drop it
- Only at -Onone
- Only with -Ounchecked
Predict first
Does it at -Osize?
import Swift
@inline(never) func add(_ a: Int, _ b: Int) -> Int { a + b }
- Yes, same trap
- No
- Only the bounds check
- It becomes a wrapping add
Predict first
And at -Ounchecked?
import Swift
@inline(never) func add(_ a: Int, _ b: Int) -> Int { a + b }
- No cond_fail at all
- Still has it
- A warning instead
- It is a compile error
Mental model. -Onone is a car with every dashboard light on. -O keeps the safety sensors but removes the wasted weight. -Ounchecked pulls the sensors out too: lighter, and you only find out about the problem when something breaks.
-Onone is the debug default: no optimisation, fast to compile, easy to step through. -O optimises for speed and is the usual release setting; -Osize prefers smaller code (less inlining and specialisation), which can be slower but helps binary size; -Ounchecked is -O plus removal of the checks. The important point is what stays: in -O and -Osize the SIL still contains cond_fail instructions for arithmetic overflow, array bounds and precondition failure, so Int.max + 1 still traps in a release build. In -Ounchecked my test file had no cond_fail at all: overflow, bounds and precondition checks are assumed never to fail, which makes a violation undefined behaviour rather than a crash. assert and assertionFailure are removed in every optimised level, while precondition and fatalError stay except in -Ounchecked.
Use -O (Xcode’s Release default) unless measurements say otherwise, try -Osize when download size matters, and avoid -Ounchecked in anything that handles untrusted input: it turns a crash into silent corruption. A related trap is that bugs can hide in one level: a data race or uninitialised read may “work” in debug and fail in release because the optimiser reorders and removes code, which is why you also run tests with optimisation.
-O keeps the trap.
-Osize also keeps it.
-Ounchecked removes the check.
Common traps
- Thinking release builds skip overflow checks. Plain
-Ostill traps; use&+when you want wrapping. - Using -Ounchecked for speed. A bounds error becomes undefined behaviour, which can be exploitable.
They might follow up with
- Where is the setting in Xcode? Build setting Swift Compiler - Code Generation, Optimization Level.
- Does -O imply whole-module? Xcode’s Release configuration enables WMO separately; see the WMO question.
Related questions
- 5.2.2 What does whole-module optimisation change, and what does it cost?
- 5.2.3 How does the inliner decide, and what do @inline(never), @inline(__always) and @inlinable do?
- 21.7.2 Where does binary size come from, how do you measure and shrink it, and what do optimisation level, stripping…
- 5.1.3 How do you look at what the compiler produced at each stage?
Where does a captured variable live: on the stack or in a heap box… →