Home

What `any` actually costs

When Swift started requiring any in front of protocol types, my first reaction was that it was ceremony. any Shape says nothing Shape did not already say, and now there is a keyword in the way.

I was wrong, and the keyword is doing exactly the job it was added for: making a cost visible that used to be invisible.

The two ways to accept a protocol

These look similar and compile to completely different things.

func render(_ shape: any Shape) { … }      // existential
func render(_ shape: some Shape) { … }     // generic (opaque parameter)

The generic version is specialised. At the call site the compiler knows the concrete type, generates code for it, and can inline everything. Calling shape.area() is a direct call.

The existential version accepts a box. any Shape is a value of unknown type, so the compiler has to store it uniformly and dispatch through a witness table. It cannot know at compile time what area() will run.

What the box looks like

An existential is laid out as three words of inline storage, plus pointers to the type metadata and the protocol witness table — five words in total on a 64-bit platform.

MemoryLayout<any Shape>.size        // 40
MemoryLayout<Circle>.size           // 8, say

If the concrete value fits in three words, it lives inline in the box. If it does not, Swift allocates it on the heap and the box holds a pointer.

That is the part worth caring about. A struct with four Double properties is 32 bytes, which does not fit in 24, so putting one into an any allocates. In a loop, that is one malloc per element:

struct Point3D { var x, y, z, w: Double }   // 32 bytes

let shapes: [any Shape] = points.map { $0 }  // heap allocation per element

Warning

The three-word threshold is an implementation detail, not a guarantee, and it is not something to design around directly. The useful takeaway is that boxing a large value type is not free, and that [any Protocol] over a big struct is a different performance shape from [ConcreteType].

When the existential is right

I want to be clear that any is not a code smell. It is the only tool for a genuinely heterogeneous collection:

var shapes: [any Shape] = [Circle(radius: 3), Square(side: 4), Triangle(...)]

A generic cannot express that. [some Shape] means “an array of one specific type I am not naming”, which is a different thing entirely — every element must be the same.

Existentials are also the right answer for stored properties that need to vary at runtime, plugin registries, and any API boundary where the concrete type is deliberately hidden from the caller.

When a generic is the better answer

The case I see written wrong most often is a function parameter:

// boxes on every call, dispatches through a witness table
func totalArea(of shapes: [any Shape]) -> Double {
    shapes.reduce(0) { $0 + $1.area() }
}

// specialised per call site, inlinable
func totalArea(of shapes: [some Shape]) -> Double {
    shapes.reduce(0) { $0 + $1.area() }
}

Both read identically. The second is faster and imposes no boxing on the caller, and the only thing it gives up is the heterogeneous case — which most functions did not need.

My rule now: start with some, and switch to any when the compiler tells you it is genuinely required. That inverts the old habit, where the protocol name alone meant existential and nobody noticed.

The self-conformance trap

The other reason to understand the box is that existentials do not conform to their own protocol, and the error message when this bites is famously unhelpful.

protocol Shape {
    func scaled(by factor: Double) -> Self
}

let shape: any Shape = Circle(radius: 3)
let bigger = shape.scaled(by: 2)     // error, before Swift 5.7

Self in a requirement means “the concrete type”, and the box does not know what that is at compile time. Modern Swift handles many of these cases by implicitly opening the existential, so this particular example works now — but the underlying limitation is still there, and protocols with associatedtype requirements still cannot be used as plain existentials without constraining the associated types.

The fix is almost always to take a generic instead:

func doubled<S: Shape>(_ shape: S) -> S {
    shape.scaled(by: 2)
}

Now Self is S, known at the call site, and the whole problem disappears.

What I actually changed

After reading through a codebase with this in mind, the changes were small and mechanical:

  • Function parameters went from any P to some P wherever the body did not need heterogeneity. Roughly forty of them, and none required any other change.
  • Stored properties stayed any P — that is what they are for.
  • Two [any P] arrays over large structs became arrays of a concrete enum instead, because there were only three possible types and an enum expresses that better than a protocol does.

None of it showed up in a profile. That is the honest ending: the wins here are real but small, and the actual value of any is that it made me notice which abstractions were load-bearing and which were habit.