How does method dispatch work in Swift?
TL;DR
Method dispatch is how the program picks which function body to run.
Swift has three kinds: static (direct call, fastest), table (vtable for classes, witness table for protocols), and message (Objective-C runtime, slowest and most dynamic).
Value types, final, private and extension-only methods use static dispatch. Overridable class methods and protocol requirements use tables.
1. Static (direct) dispatch
The compiler knows the exact function at compile time. It calls it directly. It can also inline it.
struct Calculator {
func add(_ a: Int, _ b: Int) -> Int { a + b }
}
Calculator().add(1, 2) // static: structs cannot be subclassedYou get static dispatch for:
- Methods on structs and enums.
finalclasses andfinalmethods.privateandfileprivatemethods (the compiler can see no override exists).staticmethods.- Methods that exist only in a protocol extension (not in the protocol).
2. Table dispatch
Virtual table (vtable) for classes
Each class has a table of function pointers. A subclass copies the table and replaces entries it overrides. A call looks up the table at run time.
class Animal {
func sound() -> String { "..." }
}
class Dog: Animal {
override func sound() -> String { "Woof" }
}
let pet: Animal = Dog()
pet.sound() // vtable lookup: "Woof"Witness table for protocols
Each conformance has a protocol witness table. It maps each requirement to the type's implementation. Calls through any Protocol or a generic use it.
protocol Shape { func area() -> Double }
struct Square: Shape {
let side: Double
func area() -> Double { side * side }
}
let shapes: [any Shape] = [Square(side: 2)]
shapes[0].area() // witness table lookupWhen a generic function is specialized by the optimizer, the call can become static again.
3. Message dispatch
This is the Objective-C runtime (objc_msgSend). The method is found by name at run time. It allows method swizzling and KVO.
class Player: NSObject {
@objc dynamic var score = 0 // KVO works because of message dispatch
}@objcalone exposes the method to Objective-C. Swift callers may still use the vtable.@objc dynamicforces message dispatch for every caller.
Summary table
| Declaration | Dispatch |
|---|---|
| Struct / enum method | Static |
final class / method | Static |
private / fileprivate method | Static (usually) |
| Class method that can be overridden | vtable |
Protocol requirement (via any or generic) | Witness table |
| Protocol extension method, not a requirement | Static |
@objc dynamic | Message |
The protocol extension trap
protocol Logger { func log() }
extension Logger {
func log() { print("default log") }
func flush() { print("default flush") } // not a requirement
}
struct FileLogger: Logger {
func log() { print("file log") }
func flush() { print("file flush") }
}
let logger: any Logger = FileLogger()
logger.log() // "file log" (witness table)
logger.flush() // "default flush" (static, based on the static type)To get "file flush", add func flush() to the protocol itself.
Why it matters for performance
- Static calls can be inlined. Inlining unlocks more optimizations.
- Table calls cost one extra lookup and block inlining.
- Message dispatch is the slowest, and blocks most optimizations.
Easy wins:
- Mark classes
finalwhen you do not subclass them. - Use
privatefor helpers. - Turn on Whole Module Optimization (default in Release). The compiler can then see that an
internalmethod is never overridden and make it static.
Common follow-up questions
- Does
finalreally change speed? It removes the vtable lookup and allows inlining. The gain is small per call but adds up in hot loops. - What is a protocol witness table? A per-conformance table that maps each protocol requirement to the concrete implementation.
- Why can't you override a method declared in an extension? Extension methods on Swift classes are not in the vtable. Mark them
@objc(on anNSObjectsubclass) if you really need it. - What is generic specialization? The optimizer creates a copy of a generic function for a concrete type, so calls become static.
- Which dispatch does an
@objcmethod use when Swift calls it? Usually the vtable. Objective-C callers use message dispatch. Adddynamicto force message dispatch everywhere.