How big is an any Protocol value, and when does storing one allocate on the heap?
Predict first
How wide is the container, and which struct gets boxed?
import Foundation
protocol Shape { var area: Double { get } }
struct Small: Shape { var a = 1.0, b = 2.0, c = 3.0; var area: Double { a } } // 24 bytes
struct Big: Shape { var a = 1.0, b = 2.0, c = 3.0, d = 4.0; var area: Double { a } } // 32 bytes
func blocks() -> Int { var s = malloc_statistics_t(); malloc_zone_statistics(nil, &s); return Int(s.blocks_in_use) }
@inline(never) func extraBlocks<T: Shape>(_ make: () -> T) -> Bool {
var arr: [any Shape] = []
arr.reserveCapacity(1000)
let before = blocks()
for _ in 0..<1000 { arr.append(make()) }
return blocks() - before > 500 // about one heap box per element?
}
print("any Shape:", MemoryLayout<any Shape>.size, " any Error:", MemoryLayout<any Error>.size,
" class-bound:", MemoryLayout<any Shape & AnyObject>.size)
print("Small", MemoryLayout<Small>.size, "Big", MemoryLayout<Big>.size)
print("1000 Small boxed on the heap:", extraBlocks { Small() })
print("1000 Big boxed on the heap:", extraBlocks { Big() })
- any Shape: 40, any Error: 8, class-bound: 16; Small 24, Big 32; Small boxed false; Big boxed true
- every size is 8 and both are boxed
- any Shape: 16; both boxed false
Mental model. A lunchbox with three slots: a small sandwich goes straight in, but a large cake gets a separate bag, and the box only holds the bag’s handle.
An any P for a non-class protocol is a 40-byte container on 64-bit: a three-word inline buffer for the value, a pointer to the type’s metadata, and a pointer to the protocol witness table. A class-bound existential is 16 bytes (reference plus witness table), and any Error is a single 8-byte box pointer. If the value fits in the three words, it is stored inline, with no allocation; a bigger value is copied into a heap box and the buffer holds the pointer. The check measures it with the malloc zone’s block count: appending 1000 24-byte structs adds no heap blocks, 1000 32-byte structs add about a thousand (one box each, so the test reports true). Q8 describes the container conceptually; the new point is that one extra field can change the cost from free to an allocation per value.
Consequences: arrays of [any P] are 40 bytes per element plus any boxes, and calls through a witness table cannot inline. If profiling shows existential cost, use generics or some P (monomorphised, no container), store an enum of the concrete cases, or make the large payload a class so only a reference is stored (but then copies retain). The three-word limit and the layout are implementation details measured here with Swift 6.4 on arm64, not a documented guarantee; the check only asserts the order of magnitude (about one block per element or almost none). Do not rewrite a whole codebase for this: measure with Allocations first.
Three inline words are enough for 24 bytes, but 32 bytes spill to a heap box per value.
Common traps
- Treating
any Pas free because it looks like a reference. It is five words wide and may box. - Assuming struct size is irrelevant. Crossing the inline buffer size turns each existential into an allocation.
They might follow up with
- Is
some Pboxed? No, it is the concrete type with an opaque name. - Does
any Errorallocate? Errors are always boxed, so throwing allocates a box.
Related questions
- 3.4.3 What actually allocates on the heap in Swift, and how do you see it?
- 3.4.1 What does a class instance look like in memory?
- 3.4.2 What do MemoryLayout’s size, stride and alignment mean, and why does field order matter?
- 1.4.6 What is an existential container? What is a witness table?
← Why is Optional nine bytes but Optional eight, and how do enums… What is ManagedBuffer, and how does tail allocation put a header and… →