How big are Optional, enums and existentials in memory, and why is Int? nine bytes but Bool? and a class reference optional free?

5.9.4 · L5 · Interview

Predict first

Sizes and strides (size/stride)?

protocol P { func f() }
protocol C: AnyObject { func f() }
struct Small: P { var a = 1; func f() {} }
final class K: C { func f() {} }
enum E1 { case a, b, c }
enum E2 { case a(Int), b(Int) }
enum E3 { case a(Bool), b }
enum E4 { case a(Int8), b(Int16), c }
indirect enum E5 { case leaf, node(E5, E5) }
func s<T>(_ t: T.Type) -> String { "\(MemoryLayout<T>.size)/\(MemoryLayout<T>.stride)" }
print("any P", s((any P).self), "any C", s((any C).self), "AnyObject", s(AnyObject.self), "Any", s(Any.self))
print("Int?", s(Int?.self), "Bool?", s(Bool?.self), "Int8?", s(Int8?.self), "K?", s(K?.self))
print("E1", s(E1.self), "E2", s(E2.self), "E3", s(E3.self), "E4", s(E4.self), "E5", s(E5.self), "String?", s(String?.self), "[Int]?", s([Int]?.self))
  • any P 40/40, any C 16/16, AnyObject 8/8, Any 32/32; Int? 9/16, Bool? 1/1, Int8? 2/2, K? 8/8; E1 1/1, E2 9/16, E3 1/1, E4 3/4, E5 8/8, String? 16/16, [Int]? 8/8
  • any P 8/8 and Int? 8/8
  • any P 24/24 and Bool? 2/2
  • every optional is size + 1

Predict first

Does an existential box a value larger than three words?

import Foundation
import Darwin
protocol P { var a: Int { get } }
struct Three: P { var a = 1, b = 2, c = 3 }
struct Four: P { var a = 1, b = 2, c = 3, d = 4 }
func blocks() -> Int { var s = malloc_statistics_t(); malloc_zone_statistics(nil, &s); return Int(s.blocks_in_use) }
@inline(never) func keep(_ xs: inout [any P], _ n: Int, _ make: () -> any P) { for _ in 0..<n { xs.append(make()) } }
var a: [any P] = []; a.reserveCapacity(1000)
var b: [any P] = []; b.reserveCapacity(1000)
let b0 = blocks()
keep(&a, 1000) { Three() }
let b1 = blocks()
keep(&b, 1000) { Four() }
let b2 = blocks()
print("24-byte payload: extra heap blocks", b1 - b0 >= 900 ? "about one per value" : "about none")
print("32-byte payload: extra heap blocks", b2 - b1 >= 900 ? "about one per value" : "about none")
print(MemoryLayout<Three>.size, MemoryLayout<Four>.size)
  • 24 bytes: about none; 32 bytes: about one heap block per value
  • both about one per value
  • both about none
  • 24 bytes boxed, 32 bytes inline

Mental model. An enum tag is a sticky note that can often be written on a margin the payload does not use. When the payload covers the whole page (an Int) you must staple a second sheet, and an existential is a luggage tag plus a three-pocket pouch: small items fit in the pouch, big ones go in a separate suitcase.

Swift packs an enum’s case tag into spare bits or invalid values of its payload when it can. A class reference has an invalid value (null), so K? is 8 bytes. Bool uses one byte for two values, so Bool? is still 1 byte; an enum such as E3 { case a(Bool), b } is also 1 byte. Int uses every bit, so Int? needs an extra tag byte: size 9, but stride 16 because of alignment ([Int?] costs twice an [Int]). A multi-payload enum whose payloads fill their bits, such as E2 { case a(Int), b(Int) }, is 9 bytes; E4 with Int8 and Int16 payloads is 3; an indirect enum stores a pointer, so 8. String? is 16 (strings have spare bits) and [Int]? is 8.

Existentials are bigger because they must carry the type. any P is 40 bytes: a three-word inline buffer plus a metadata pointer plus a witness-table pointer. A value of up to 24 bytes lives inline; a larger one is boxed on the heap. My run allocated about nothing for a 24-byte struct and about one block per value for a 32-byte one. A class-bound any C is 16 (reference plus witness table), AnyObject is 8, Any is 32 (buffer plus metadata, no witness table). The practical advice: prefer generics or some in hot paths, keep structs you store as existentials at or under three words, and remember that MemoryLayout.size is the packed size while stride is what an array uses.

Measured on arm64 macOS with Swift 6.4.

The inline buffer is three words; larger values go to the heap.

Common traps

  • Assuming Optional adds one byte to anything. It is free for types with spare bits and costs a padded word for Int, Double and similar.
  • Storing large structs as any P in arrays. Each element beyond three words is a separate heap allocation.

They might follow up with

  • What is the layout of a class instance? A 16-byte header (metadata pointer and refcount) followed by properties.
  • Why does field order matter? See the MemoryLayout question linked below.

Related questions

  • 1.4.6 What is an existential container? What is a witness table?
  • 3.4.2 What do MemoryLayout’s size, stride and alignment mean, and why does field order matter?
  • 5.3.3 What is different in the compiled code between returning some P and any P?
  • 5.3.2 How does an unspecialised generic function know the concrete type, and what does that cost?

← What do @guaranteed and @owned mean in SIL, which does Swift use by…