Why is Optional nine bytes but Optional eight, and how do enums choose their layout?

3.10.1 · L5 · Learning

Predict first

Which of these optionals and enums add storage?

class C {}
enum Dir { case n, e, s, w }
enum Payload { case int(Int); case flag(Bool); case none }
enum Ref { case obj(C); case none }
indirect enum Tree { case leaf; case node(Tree, Int, Tree) }
func sz<T>(_ t: T.Type, _ name: String) { print(name + ":", MemoryLayout<T>.size, MemoryLayout<T>.stride) }
sz(Bool.self, "Bool"); sz(Bool?.self, "Bool?")
sz(Int.self, "Int"); sz(Int?.self, "Int?"); sz(Int??.self, "Int??")
sz(C.self, "C"); sz(C?.self, "C?")
sz(String?.self, "String?")
sz(Dir.self, "Dir"); sz(Dir?.self, "Dir?")
sz(Payload.self, "Payload"); sz(Ref.self, "Ref"); sz(Tree.self, "Tree")
  • Int? is 9 (stride 16), Int?? 10, but Bool?, C?, String?, Dir? and Ref add nothing; Payload is 9 (stride 16); Tree is 8
  • Every Optional adds one byte
  • Only classes add storage

Mental model. A parking garage with some numbered bays that never hold cars can use those bays as “reserved” signs without adding a new floor. When every bay is a valid car, “empty” needs its own extra marker.

A value’s bit pattern often has spare patterns that no valid value uses (extra inhabitants), and Swift stores enum cases and nil in them when it can. A class reference is a non-null pointer with many invalid patterns, so C? costs nothing (8 bytes), and so does a two-case enum with a class payload (Ref, 8). A Bool uses 2 of 256 patterns, so Bool? is still 1 byte, and a 4-case enum Dir is 1 byte with Dir? also 1. But every pattern of an Int is valid, so Int? needs an extra tag byte: size 9, though stride stays 16 for alignment (size is the bytes used, stride the spacing in an array). Int?? still fits the single extra byte (size 10). A multi-payload enum (Payload) uses the larger payload plus a tag byte. An indirect enum stores its payload in a heap box, so the value is a single pointer (8).

Practical points: do not hand-reorder properties for pennies, but do remember that arrays of Int? or Double? take 16 bytes per element, so a million optional numbers cost 16 MB, whereas a sentinel value or a bit set is far smaller; String? and [T]? are free because strings and arrays have spare patterns. The exact packing is an ABI-stable detail for the standard library, but for your own enums it is the compiler’s choice and may change, so use MemoryLayout to measure rather than to rely on it (Q145 explains size, stride and alignment).

Spare bit patterns hold nil and case tags; Int has none, so Int? needs a tag byte.

Common traps

  • Assuming Optional<T> always adds a byte. It does not when T has spare bit patterns.
  • Using size to compute array memory. Arrays use stride.

They might follow up with

  • Why are enums with only case labels one byte? They need only a tag with enough bits for the case count.
  • Does indirect cost a heap allocation per node? Yes, each payload box is allocated.

Related questions

  • 3.4.2 What do MemoryLayout’s size, stride and alignment mean, and why does field order matter?
  • 3.4.1 What does a class instance look like in memory?
  • 3.4.3 What actually allocates on the heap in Swift, and how do you see it?

How big is an any Protocol value, and when does storing one allocate… →