Swift Internals: Why any Protocol Can Be Slower Than Generics
Why Can Generics Be Faster Than any Protocol in Swift?
In the previous article, we went under the hood of the Swift Runtime and looked at how method calls can use direct dispatch, VTables, witness tables, and Objective-C message dispatch.
We also saw that protocols can use witness table dispatch.
That leads to an interesting question:
Why can
any Protocolbe slower than generics?
Consider these two functions:
func process(_ value: any Drawable) {
value.draw()
}and:
func process<T: Drawable>(_ value: T) {
value.draw()
}They look almost identical.
But underneath, Swift can treat them very differently.
The short version is:
Generics
↓
Compiler can specialize for concrete types
↓
Potentially direct call / inlining
↓
Fastany Protocol
↓
Runtime existential container
↓
Witness table lookup
↓
Indirect call
↓
Potentially more overheadBut there’s a much more interesting story behind this.
The Generic Version
Let’s start with:
protocol Drawable {
func draw()
}
struct Circle: Drawable {
func draw() {
print("Drawing Circle")
}
}
func process<T: Drawable>(_ value: T) {
value.draw()
}And:
let circle = Circle()
process(circle)The compiler knows that this particular call is:
T = CircleThat gives the compiler an opportunity to specialize the generic function.
Conceptually, it can produce something similar to:
func process_Circle(_ value: Circle) {
value.draw()
}The compiler doesn’t literally have to generate a Swift function with that exact name, but this is a useful mental model.
So instead of one completely generic implementation, the optimizer can potentially create a specialized version for Circle.
Generic Function
process<T: Drawable>
↓
Compiler sees:
T = Circle
↓
Specialization
↓
process specialized for Circle
↓
Circle.draw()Now the compiler has much more information.
It may be able to:
- eliminate witness table lookups
- devirtualize the call
- inline
draw() - propagate constants
- eliminate unnecessary abstraction
Potentially turning:
generic call
↓
witness table
↓
indirect function callinto something closer to:
direct call
↓
Circle.draw()This is one of the major reasons specialized generics can be extremely fast.
But What Happens With any Drawable?
Now look at:
func process(_ value: any Drawable) {
value.draw()
}And:
let circle: any Drawable = Circle()
process(circle)The function doesn’t receive a Circle.
It receives:
any DrawableThe concrete type could be:
Circle
Square
Triangle
...The function has to work with all of them.
This is called an existential value.
Swift needs a runtime representation that can carry:
- The actual value
- Information about its concrete type
- Information about how that type satisfies the protocol
That’s where the existential container comes in.
The Existential Container
A simplified existential container can be visualized as:
┌─────────────────────────────┐
│ Value Buffer │
│ │
│ 3 machine words │
├─────────────────────────────┤
│ Type Metadata Pointer │
├─────────────────────────────┤
│ Witness Table Pointer │
└─────────────────────────────┘For a typical Swift value existential, the inline value buffer provides 3 machine words of storage.
So conceptually:
any Drawable
┌─────────────────────────────┐
│ Word 1 │
│ Word 2 │
│ Word 3 │
├─────────────────────────────┤
│ Type Metadata │
├─────────────────────────────┤
│ Witness Table │
└─────────────────────────────┘The three-word buffer is important because Swift tries to keep small values inside the existential container itself.
That can avoid a separate heap allocation.
What If the Value Doesn’t Fit?
Suppose our type is small:
struct Point: Drawable {
var x: Int
var y: Int
}It can potentially fit inside the inline buffer.
Conceptually:
any Drawable
┌─────────────────────┐
│ Point.x │
│ Point.y │
│ unused buffer │
├─────────────────────┤
│ Type Metadata │
├─────────────────────┤
│ Witness Table │
└─────────────────────┘But imagine a much larger value:
struct LargeObject: Drawable {
var data: ...
}If the value doesn’t fit in the inline buffer, Swift can use out-of-line storage, meaning the existential can contain a reference to a heap-allocated box containing the actual value.
Conceptually:
Existential Container
┌─────────────────────┐
│ Pointer ────────────┼────→ Heap Box
├─────────────────────┤
│ Type Metadata │
├─────────────────────┤
│ Witness Table │
└─────────────────────┘Now there can be additional costs associated with heap allocation and reference management.
So the three-word inline buffer is one of the mechanisms Swift uses to balance performance and memory efficiency.
Where Does the Witness Table Come In?
Remember our previous article.
For:
protocol Drawable {
func draw()
}and:
struct Circle: Drawable {
func draw() {
print("Circle")
}
}Swift can have a conformance describing:
Circle → DrawableConceptually:
Circle → Drawable Witness Table┌────────────────────────┐
│ draw → Circle.draw │
└────────────────────────┘The existential container carries a reference to the appropriate witness table.
So when we write:
value.draw()where:
value: any Drawablethe runtime can conceptually do:
any Drawable
↓
Existential Container
↓
Witness Table
↓
draw slot
↓
Circle.draw()The important point is that the implementation is selected indirectly through the witness table.
Why Can This Be Slower?
Compare the two cases.
Generic
func process<T: Drawable>(_ value: T) {
value.draw()
}If specialized for Circle:
T = Circle
↓
Specialized code
↓
Circle.draw()Potentially:
Direct Call
↓
InliningExistential
func process(_ value: any Drawable) {
value.draw()
}Conceptually:
any Drawable
↓
Existential Container
↓
Witness Table
↓
Function Pointer
↓
Circle.draw()That additional level of indirection can make the existential version slower in some situations.
Get Kousik Dasari’s stories in your inbox
Join Medium for free to get updates from this writer.
And because the concrete type is intentionally hidden behind any Drawable, the optimizer has less information to work with.
But There Is an Important Catch
We should not say:
“any Protocolis always slower than generics."
That’s incorrect.
The real statement is:
any Protocolcan introduce runtime overhead that specialized generics may avoid.
There are several factors involved:
- whether the generic function gets specialized
- optimization settings
- whether the value fits in the inline buffer
- whether heap boxing is required
- whether the protocol call can be optimized
- whether the operation itself dominates the cost
A generic function that cannot be specialized may also involve runtime metadata and witness tables.
So the comparison isn’t simply:
Generic = fast
any = slowIt is:
Specialized Generic
↓
Potentially very little runtime overheadExistential
↓
Runtime representation + dynamic protocol dispatchWhy Doesn’t Swift Specialize Everything?
Now we reach an important compiler-design trade-off.
Imagine this:
func process<T: Drawable>(_ value: T) {
value.draw()
}And your application calls it with:
Circle
Square
Triangle
Rectangle
Hexagon
...If the compiler generated a completely specialized version for every concrete type:
process_Circle
process_Square
process_Triangle
process_Rectangle
process_Hexagon
...the generated machine code could become much larger.
This is called code bloat.
More specialization can mean:
More specialization
↓
More optimization opportunities
↓
Potentially faster codeBut:
More specialization
↓
More generated machine code
↓
Larger binarySwift’s compiler has to find a reasonable balance.
Generic Specialization vs Code Size
Consider:
func process<T: Drawable>(_ value: T) {
value.draw()
}If Swift specializes it for every possible type, we might get:
process<Circle>
process<Square>
process<Triangle>
process<Rectangle>
...That could improve performance.
But if there are hundreds of concrete types, generating hundreds of copies of the function may not be worth the additional binary size.
So the compiler can make decisions about when specialization is beneficial.
The important idea is:
Swift doesn’t blindly specialize every generic function for every type.
It tries to balance optimization opportunities against generated code size and compilation costs.
This Is Where any Has a Purpose
If generics can be so fast, why does Swift even have:
any DrawableBecause existential types provide something generics don’t:
Type erasure.
You can put different concrete types behind the same interface:
let shapes: [any Drawable] = [
Circle(),
Square(),
Triangle()
]Now the collection doesn’t need to know the concrete type of every element at compile time.
Conceptually:
[any Drawable]┌───────────────┐
│ Circle │
│ Witness Table │
├───────────────┤
│ Square │
│ Witness Table │
├───────────────┤
│ Triangle │
│ Witness Table │
└───────────────┘This flexibility comes with runtime machinery.
And that’s the trade-off.
Generics vs any Protocol
Let’s summarize the difference.
Generics
func process<T: Drawable>(_ value: T)The concrete type remains part of the generic abstraction.
Conceptually:
T = Circle
↓
Compiler can specialize
↓
Circle-specific code
↓
Potential direct dispatchExistential
func process(_ value: any Drawable)The concrete type is hidden behind an existential.
Conceptually:
any Drawable
↓
Existential Container
↓
Type Metadata
↓
Witness Table
↓
Indirect callSo:
Generic
→ "Keep the concrete type information available."
any Protocol
→ "Hide the concrete type behind a common interface."The Bigger Swift Design Philosophy
This is actually a great example of how Swift balances abstraction and performance.
Swift gives you multiple choices:
Concrete Type
↓
Maximum static information
↓
Potential direct dispatchGeneric
↓
Static abstraction
↓
Potential specialization
↓
Potentially very efficientany Protocol
↓
Runtime abstraction
↓
Existential Container
↓
Witness Table
↓
More flexibilityYou choose the abstraction based on what you need.
If you need a heterogeneous collection:
let items: [any Drawable]is incredibly useful.
If you want an API where the concrete type can remain known to the compiler:
func process<T: Drawable>(_ value: T)generics may provide more optimization opportunities.
The Mental Model
The easiest way to remember all of this is:
Protocol
│
┌────────┴────────┐
│ │
Generic any Protocol
│ │
↓ ↓
Concrete T Existential Container
│ │
↓ ┌──────┴──────┐
Specialization │ │
│ Metadata Witness Table
↓ │ │
Optimized Code └──────┬──────┘
│ ↓
│ Indirect Call
↓
Potential Direct CallSo the real reason generics can be faster isn’t because generics magically have a faster dispatch mechanism.
It’s because:
Generics preserve type information that the compiler can use for specialization and optimization, while
any Protocolintentionally hides the concrete type behind an existential container.
And Swift’s compiler constantly balances this against another concern:
How much optimization is worth the extra generated code?
That’s the trade-off between runtime performance, binary size, and abstraction.
What’s Next?
We’ve now seen how any Protocol uses an existential container and witness table dispatch, and why specialized generics can sometimes avoid those costs.
But there is another interesting question:
What exactly happens to a generic function when Swift cannot specialize it?
That takes us deeper into generic metadata, runtime type information, reabstraction, and the difference between specialized and unspecialized generic code.
That’s where the Swift Runtime gets even more interesting.






