What do -Onone, -O, -Osize and -Ounchecked each change, and which safety checks survive in a release build?

5.9.1 · L3 · Interview

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 -O still 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… →