iosNudge

How does method dispatch work in Swift?

HardSwiftPerformance 3 min read

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 subclassed

You get static dispatch for:

  • Methods on structs and enums.
  • final classes and final methods.
  • private and fileprivate methods (the compiler can see no override exists).
  • static methods.
  • 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 lookup

When 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
}
  • @objc alone exposes the method to Objective-C. Swift callers may still use the vtable.
  • @objc dynamic forces message dispatch for every caller.

Summary table

DeclarationDispatch
Struct / enum methodStatic
final class / methodStatic
private / fileprivate methodStatic (usually)
Class method that can be overriddenvtable
Protocol requirement (via any or generic)Witness table
Protocol extension method, not a requirementStatic
@objc dynamicMessage

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 final when you do not subclass them.
  • Use private for helpers.
  • Turn on Whole Module Optimization (default in Release). The compiler can then see that an internal method is never overridden and make it static.

Common follow-up questions

  • Does final really 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 an NSObject subclass) 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 @objc method use when Swift calls it? Usually the vtable. Objective-C callers use message dispatch. Add dynamic to force message dispatch everywhere.