Swift Dispatch Demystified — What Really Happens Under the Hood.
Background
Coder Orangu remembers watching the WWDC session when Apple unveiled Swift for the very first time. The keynote buzzed with excitement — a new syntax, strong safety guarantees, and modern language features. But what stood out the most was Apple’s repeated emphasis on performance. Swift wasn’t just designed to be elegant; it was built to be fast.
Coder Orangu grew curious—what changes had Apple made beneath the surface that made Swift faster than Objective-C? One of the major reasons Apple highlighted was that Swift is a statically typed language. By knowing types at compile time, the compiler can make stronger optimisations, avoid unnecessary runtime checks, and generate faster code.
But type safety was only part of the story. Another crucial factor behind Swift’s speed lies in method dispatch — the process of deciding which method to call, either at compile time (static dispatch) or at runtime (dynamic dispatch).
Method dispatch is the process a language uses to decide which actual function body runs when you call a method. A single method name might have several possible implementations—defined in superclasses, subclasses, or protocol extensions.
Static Dispatch
Static dispatch occurs when the compiler decides in advance which method will be called. Since the choice is fixed before the program runs, there’s no extra decision-making during execution. This makes static dispatch faster and more efficient.
However, the compiler can only use static dispatch in specific cases—typically when polymorphism isn’t involved. If a method can be overridden or behaves differently depending on the object type, then the decision must be deferred to runtime.
A good example is a final
class or a class without inheritance. In these cases, methods are guaranteed not to be overridden, allowing the compiler to use static dispatch safely. The same applies to value types (struct and enum). Since they don’t support inheritance, their methods are statically dispatched — unless accessed through a protocol reference, which switches to dynamic dispatch.
Dynamic Dispatch
Dynamic dispatch is a process in which the method to be called is decided at runtime. Instead of the compiler fixing the call in advance, the program looks up the correct method while running.
To achieve this, the runtime typically relies on a virtual table (vtable) generated by the compiler. A vtable is essentially an array of function pointers, where each entry points to the actual implementation of a method for that specific class. When a method is invoked, the program consults the vtable to find and call the correct function.
This lookup introduces a small runtime cost compared to static dispatch, but it provides the flexibility required for inheritance, method overriding, and protocol-based polymorphism.
A common case of dynamic dispatch is calling a method that can be overridden—such as a class method in an inheritance chain. The program decides at runtime which implementation to run, ensuring the correct override is used.
Metaphor
Coder Orangu compares Static Dispatch to sitting on a branch where the fruit grows on the same branch — the path is clear and known beforehand, just as the compiler already knows exactly which function to call.
In contrast, Dynamic Dispatch is like reaching for a fruit hanging from a nearby branch — the goal remains the same, but it requires a careful branch jump, introducing a slight delay before you can enjoy the reward.
How Dispatch Works
Static Dispatch
Coder Orangu now had a basic idea of what static dispatch is, but a new question arose—how does the Swift compiler actually make it happen? Let’s see what happens behind the scenes when you write a simple method call and build.
Here’s what really happens:
The call is resolved at compile time because the compiler knows the exact function body of
area
.
The optimizer can inline the method, so the generated machine code performs the multiplication directly—no extra function call needed.
What finally runs is raw machine instructions, not another Swift function.
Because the compiler knows the exact method ahead of time, it can choose to inline the code—placing the method’s instructions directly at the call site, avoiding the overhead of setting up and tearing down the call stack. Even if it doesn’t inline, the call still avoids any runtime lookup or v-table access. This lack of extra steps is what makes static dispatch faster than dynamic dispatch.
How Dynamic Dispatch Works
Coder Orangu now understands that when the compiler can’t decide the method call at compile time, it must defer the choice until the program runs. This is where dynamic dispatch steps in to preserve flexibility.
Let’s see what the compiler sets up behind the scenes:
Here’s what really happens:
The compiler cannot decide the exact method at compile time, because shape is declared as Shape but could actually hold any subclass (e.g., Rectangle, Circle).
To handle this flexibility, the compiler generates a hidden field for every class instance — a pointer to that class’s type information (metadata) stored in static memory.
This type information contains a virtual method table (vtable), mapping methods like area to their actual implementations.
The method call is replaced with a vtable lookup, which conceptually becomes
shape.type.vtable.area(shape)
Behind the scenes, the compiler generates a struct to keep the metadata of every class. This struct holds essential details like the type information, method table (vtable), reference counts, and inheritance details. Whenever you call a method on a class instance, the compiler doesn’t directly jump to the method—it first looks into this structure to figure out the right function pointer. That’s why dynamic dispatch is possible.
I’ve already written a detailed breakdown of this class structure and memory layout here: Behind the Curtain: Swift Class Structure
Applicability
Static and dynamic dispatch are not about choosing which is “better.” —and in most cases, there’s no direct choice for the compiler to use one instead of the other. The compiler decides based on the language rules and context. However, developers can guide the compiler toward static dispatch in some cases to improve performance or clarity.
Let’s look at how each applies in real Swift scenarios.
Static Dispatch Cases
Coder Orangu loves to help the compiler so it can make decisions ahead of time, rather than waiting for runtime. This not only brings clarity and speed but also keeps behavior predictable — something every seasoned Swift developer appreciates.
Let’s look at situations where static dispatch occurs:
Methods on value types
All functions in struct and enum are statically dispatched by default because they cannot be subclassed. Except when they are called through a protocol reference — in that case, Swift switches to dynamic dispatch using a witness table.
1
2
3
4
5
6
7
8
9
struct Rectangle {
let width: Double
let height: Double
func area() -> Double {
return self.width * self.height
}
}
let rect = Rectangle(width: 10.0, height: 5.0)
rect.area() // ✅ Static dispatch
Final Classes or Methods
All methods in a class marked with the keyword final
are statically dispatched. Because a final class cannot be subclassed, the compiler knows at compile time exactly which implementation to call — so it skips the dynamic dispatch table (vtable).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class Rectangle {
let width: Double
let height: Double
func area() -> Double {
return self.width * self.height
}
func perimeter() -> Double {
return 2 * (self.width + self.height)
}
}
let rect = Rectangle(width: 10.0, height: 5.0)
rect.area() // ✅ Static dispatch
rect.perimeter() // ✅ Static dispatch
Protocol Extension Methods
All methods implemented inside a protocol extension and not part of the protocol’s original requirement are statically dispatched. This means that the compiler decides which implementation to call at compile time, based on the static type of the variable — not the runtime type.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protocol Shape {
func area() -> Double
}
extension Shape {
func describe() {
print("This is a shape")
}
}
struct Rectangle: Shape {
let width: Double
let height: Double
func area() -> Double { self.width * self.height }
func describe() {
print("This is a rectangle")
}
}
let shape: Shape = Rectangle()
shape.area() // ⭐ Dynamic dispatch (protocol requirement)
shape.describe() // ✅ Static dispatch (from protocol extension)
Dynamic Dispatch Cases
Coder Orangu also wants the flexibility provided by polymorphism, but it comes with a runtime cost — every method call must be resolved dynamically through a lookup mechanism such as a vtable (for classes) or a witness table (for protocols). Dynamic dispatch allows Swift to determine which implementation of a method to call at runtime, based on the actual type of the instance, not the declared type.
Overridable Class Method
When a class method is not marked as final, Swift uses dynamic dispatch through a vtable (virtual method table).This allows subclasses to override methods and provide their own implementations — enabling polymorphic behavior.
This flexibility helps in building extensible systems, where new functionality can be introduced simply by subclassing and overriding existing methods, without modifying the original codebase.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Shape {
func draw() {
print("Drawing a shape")
}
}
class Circle: Shape {
override func draw() {
print("Drawing a circle")
}
}
let shape: Shape = Circle()
shape.draw() // ⭐ Dynamic dispatch
Protocol Required Methods
When a method is defined in a protocol, any type that conforms to that protocol must provide an implementation. Swift uses dynamic dispatch for these methods, allowing the actual implementation to be determined at runtime, based on the conforming type. However, Unlike vtables for classes, swift uses witness tables for protocol conformances, ensuring type-safe dynamic dispatch without relying on Objective-C runtime features.
This approach is widely used to design flexible and decoupled architectures, where components interact through common interfaces rather than concrete implementations.
1
2
3
4
5
6
7
8
9
10
11
12
protocol Shape {
func area() -> Double
}
struct Rectangle: Shape {
let width: Double
let height: Double
func area() -> Double { self.width * self.height }
}
let shape: Shape = Rectangle()
shape.area() // ⭐ Dynamic dispatch
Summary
Here’s how the compiler decides under the hood — summarized neatly by Coder Orangu:
Case | Dispatch Type | Reason |
---|---|---|
Struct and Enum Methods | Static | They can’t be subclassed; compiler knows exact implementation. |
Final Class Methods | Static | Class marked as final prevents overriding, so no vtable lookup. |
Protocol Extension Methods | Static | Not part of protocol requirements; resolved at compile time. |
Overridable Class Methods | Dynamic | Uses vtable to allow subclasses to override behavior. |
Protocol Required Methods | Dynamic | Actual implementation resolved at runtime through witness table. |
Final Thought
Coder Orangu believes swift gives you fine-grained control over how functions are dispatched — balancing performance and flexibility.
Static dispatch offers speed and predictability.
Dynamic dispatch provides extensibility and polymorphism.
Developer like Coder Orangu thoughtfully can chooses between them intentionally — marking classes or methods as final when extension is not needed, and relying on protocols or overridable methods when designing for growth.
Takeaway
Use final when you don’t need inheritance.
Prefer value types for performance-critical paths.
Embrace protocols for flexibility — but understand what you trade off in dispatch cost.
Comments powered by Disqus.