When is a Swift global or static property initialised, what is swift_once, and when does the optimiser make the initialiser disappear?
Predict first
What does this print?
let eager: Int = { print("top-level let"); return 1 }()
struct C { static let lazy: Int = { print("static let"); return 2 }() }
print("start")
_ = C.lazy; _ = C.lazy
print("end")
- top-level let / start / static let / end
- start / top-level let / static let / end
- top-level let / start / static let / static let / end
- start / static let / top-level let / end
Predict first
At -Onone, do limit and table have lazy-initialisation tokens in SIL?
import Swift
public let table = [1, 2, 3]
public let limit = 100
public let shared = Cfg()
public final class Cfg: Sendable { public init() {} }
public func use() -> Int { limit + table.count + (shared === shared ? 1 : 0) }
- Yes: private _Wz tokens for each global
- No: they are plain data
- Only for the class instance
- Only in -O
Predict first
At -O, do limit and table still have tokens?
import Swift
public let table = [1, 2, 3]
public let limit = 100
public let shared = Cfg()
public final class Cfg: Sendable { public init() {} }
public func use() -> Int { limit + table.count + (shared === shared ? 1 : 0) }
- No: they become statically initialised; the class instance keeps its token
- Yes, all three
- No tokens at all, including the class instance
- Only table keeps one
Mental model. A lazy global is a kettle filled when somebody first wants tea, with a note taped on it so nobody boils it twice. If the “tea” is a fixed packet that comes already poured (a constant), the optimiser delivers it ready-made and removes the note.
Globals and static let properties in a normal file are lazy: the initialiser runs the first time the value is read, exactly once, even if several threads race. The compiler generates an accessor (an addressor) that checks a one-word token and calls swift_once on the first use; later reads pay a load and a compare. The exception is the top-level code of main.swift: its variables are initialised eagerly, in order, as script statements. The run below shows the difference: the top-level let prints before start, while the static let prints only when first touched (and only once).
In -Onone every global gets a once token (sil_global private @…_Wz in the SIL). With -O the optimiser can turn an initialiser that is a compile-time constant, such as an integer or an array of integer literals, into statically initialised data, and the token and the call disappear: in my build limit and table had a static value and no token, while an instance of a class (shared) still kept its token. Practical consequences: do not put side effects in global initialisers, because you cannot tell when they run; an expensive global is paid for at the first access, which may be on the main thread during a scroll; and “thread-safe lazy singleton” is the default behaviour, not something to add a lock for.
Script-level variables initialise in order before later statements; the static let initialises once, on first use.
Every global gets a token in -Onone.
Constant initialisers are folded into static data; the class instance still goes through a once token in this build.
Common traps
- Relying on a global’s initialiser to run at launch. It runs at first use, not at app start; only
main.swifttop-level code is eager. - Assuming initialisation order between globals in different files. There is none; each runs when first read, which can create hidden dependency chains.
They might follow up with
- Is a
static letthread-safe to read? The initialisation is; the value’s own mutability is separate (Swift 6 checks Sendable). - How do I run something at launch then? Do it explicitly from the app entry point, not through a global.
Related questions
- 5.3.2 How does an unspecialised generic function know the concrete type, and what does that cost?
- 5.2.4 What is exclusivity enforcement, and what does it cost at runtime?
- 5.2.3 How does the inliner decide, and what do @inline(never), @inline(__always) and @inlinable do?
- 10.4.2 How do @MainActor, global actors, isolated and nonisolated work, and how does isolation propagate?
- 10.9.5 What does the Swift 6.2 default actor isolation setting change, and how does it affect a global variable or…
← How do access levels become linkage in the object file, and why can… What reflection metadata does the compiler emit for every type, what… →