How does Swift optimize enums with associated values?
The baseline: enums are tagged unions
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case point
}
Naively: storage for the largest case’s payload plus a discriminator tag.
1. Tag-bit packing into spare bits
The compiler tries to steal unused bit patterns from the payload itself to encode the case discriminator (spare bit optimization) — often zero extra size.
2. The single-payload case — extreme version: Optional
For Optional<SomeClass>, the compiler exploits that a valid object pointer is never 0 — repurposing the all-zero bit pattern for .none. Result: Optional<SomeClass> is exactly the same size as a raw pointer — zero overhead.
3. Indirect enums — for recursive/self-referential cases
indirect enum Expression {
case literal(Int)
case add(Expression, Expression)
case multiply(Expression, Expression)
}
indirect boxes that case’s payload onto the heap (a single pointer inline), making recursive value types representable. Per-case indirect case boxes only that case.
4. Layout is determined per concrete instantiation for generic enums
Optional<Int> and Optional<SomeHugeStruct> have completely different concrete layouts, via the value-witness table.
5. Copy/destroy cost follows payload, not the enum wrapper itself
The enum wrapper itself adds no additional ARC overhead beyond whatever its currently-active payload requires.
Summary table
| Optimization | What it targets | Effect |
|---|---|---|
| Naive tagged union | Baseline | Size = largest payload + tag |
| Spare bit packing | Payloads with unused bit patterns | Tag folded into existing bits |
| Optional’s null-pointer optimization | Class references | Same size as raw pointer |
indirect | Recursive payloads | Heap-boxes specific case(s) |
| Per-instantiation layout (generics) | Generic enums | Concrete layout computed per T |
| Payload-driven copy/destroy | ARC / COW cost | Zero overhead beyond active payload |
One-liner if pressed: “Swift lays enums out as tagged unions sized to the largest case, but aggressively optimizes the tag away into spare bits of the payload when possible — the extreme case being Optional of a class reference, literally the same size as a raw pointer — and falls back to heap-boxing via indirect only when a case’s payload is recursively self-referential.”