How much slower is a Debug (-Onone) build than Release (-O), why, and what does that mean for profiling and for what you ship?

21.8.2 · L3 · Interview

Predict first

Build one workload with -Onone and -O (the program invokes swiftc itself) and compare.

import Foundation

let workload = """
import Foundation
struct P { var x: Double; var y: Double }
func total<C: Collection>(_ c: C) -> Double where C.Element == P { c.reduce(0) { $0 + $1.x * $1.y } }
let pts = (0..<1000).map { P(x: Double($0), y: 1.5) }
var acc = 0.0
let t = DispatchTime.now()
for _ in 0..<2000 { acc += total(pts.lazy.filter { $0.x > 10 }.map { P(x: $0.x, y: $0.y + 1) }) }
print(Double(DispatchTime.now().uptimeNanoseconds - t.uptimeNanoseconds) / 1e6, acc > 0)
"""
let dir = NSTemporaryDirectory() + "optcmp-\(getpid())"
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
try! workload.write(toFile: dir + "/w.swift", atomically: true, encoding: .utf8)

func run(_ exe: String, _ args: [String]) -> String {
    let p = Process(), out = Pipe()
    p.executableURL = URL(fileURLWithPath: exe); p.arguments = args; p.standardOutput = out
    try! p.run(); p.waitUntilExit()
    return String(decoding: out.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
}
var times: [String: Double] = [:]
for flag in ["-Onone", "-O"] {
    _ = run("/usr/bin/swiftc", [flag, dir + "/w.swift", "-o", dir + "/w\(flag)"])
    times[flag] = (0..<3).map { _ in Double(run(dir + "/w\(flag)", []).split(separator: " ")[0])! }.min()!
}
print("the same source built with -Onone is more than 5x slower than -O:", times["-Onone"]! > 5 * times["-O"]!)
try? FileManager.default.removeItem(atPath: dir)
  • the same source built with -Onone is more than 5x slower than -O: true
  • The two builds run at the same speed
  • -Onone is faster because it skips optimisation work

Mental model. Rehearsing a play with every actor reading from a script and stopping to explain each move. You learn the plot, not the running time.

Debug builds optimise for compile speed and debuggability: no inlining, no specialisation of generics, no removal of retain/release pairs, extra checks, and every variable kept alive for the debugger. Generic code written with protocols and closures (reduce, lazy.filter.map, a generic Collection function) pays the most, because a Release build specialises and inlines it into a tight loop. In the check, the same source built both ways ran more than five times slower at -Onone; on the machine that produced this answer the gap was over a hundred times for this closure-heavy workload, which is why the check asserts only a generous bound. Tight loops over plain integers are closer, and the gap depends entirely on how much abstraction the code uses.

Consequences: profile Release builds (Instruments defaults to a Release-like profile for this reason), because a Debug trace points at overhead that disappears under optimisation and hides what remains. Do not conclude “SwiftUI is slow” or “my parser is slow” from a Debug run. Note the other direction too: Debug builds can mask or exaggerate timing-dependent bugs, and Release can reorder lifetimes (Q459). -Osize trades speed for a smaller binary and -Ounchecked removes overflow and bounds checks (do not ship it without a reason and measurements). What is not asserted here: absolute ratios, which depend on the compiler and the CPU.

Debug builds keep every abstraction layer that Release removes.

Common traps

  • Profiling a Debug build. The hot spot may be pure Debug overhead.
  • Comparing two implementations in Debug. The ranking can differ in Release.

They might follow up with

  • Why is a generic function slower in Debug? It runs through witness tables and boxed values because specialisation is an optimisation pass (Q27).
  • Should I ever ship -Ounchecked? Rarely; you give up traps that turn bugs into crashes rather than corruption.

Related questions

  • 21.8.1 Your micro-benchmark reports an absurdly fast time. What did the optimiser probably do, and how do you write…
  • 21.8.3 How do you find out how an operation really scales, and which Swift operations that look cheap are quadratic?
  • 21.1.1 How do you approach a performance problem, and which kinds of problem are there?
  • 23.3.2 Why do some bugs appear only in Release builds, how do assert, precondition and fatalError differ, and how do…
  • 1.4.7 What is generic specialization? When does it not happen?

← Your micro-benchmark reports an absurdly fast time. What did the…   How do you find out how an operation really scales, and which Swift… →