You may have heard that adding final to a class makes it faster. It sounds almost like an urban legend, but there is a precise mechanism behind it.
This continues from the previous article Advanced Swift #4.
The seemingly obvious act of calling a method is compiled in three different ways. The chosen form determines performance and optimization opportunities.
This installment of the advanced series is about dispatch.
Three faces of a call — direct, table, and message
There are three ways code such as object.doSomething() can be translated into machine code.
Static dispatch. The function is known at compile time, so the call jumps directly to its address.
It is the fastest. More importantly, the compiler can inline it—remove the call and embed the body—opening the door to further optimization. Methods on structs, global functions, and final methods use it.
Table dispatch. This is how class methods that support inheritance and overriding work.
Even if a variable’s compile-time type is Animal, its actual instance may be Dog. Which implementation to call is known only at runtime.
Each class therefore has a virtual function table (vtable). At the call site, the runtime finds the function’s index in the instance’s table and jumps there. Polymorphism costs one level of indirection.
Protocols have a counterpart called a witness table, which serves the same role. It is the table you saw in the some and any discussion.
Message dispatch. This is the Objective-C runtime approach, where calls are handled by name-based lookup through objc_msgSend.
It is the slowest of the three, but buys extreme flexibility, including replacing methods at runtime. In Swift, methods marked @objc dynamic enter this world.
The internals of objc_msgSend were covered in a separate article, so we will leave them aside here.
Ultimately, it is a trade-off between flexibility and speed. The more runtime-determined a call is, the slower it is; the more fixed it is at compile time, the faster and more optimizable it becomes.
Why final and private improve performance
We can now dissect the urban legend. When can the compiler statically lower table dispatch through devirtualization?
It must prove that “this method cannot be overridden.”
final is exactly that proof. final class and final func declare that subclasses cannot redefine them.
The compiler can remove the vtable lookup and replace it with a direct call, also opening the door to inlining (Swift Optimization Tips).
private has a similar effect. A declaration invisible outside the file lets the compiler see every use within that file and prove that it is not overridden.
When whole-module optimization (WMO, the default for modern release builds) is enabled, the proof extends across the entire module. An internal class without subclasses is automatically treated as final.
The practical rule is simple: mark classes final when you do not plan to subclass them.
This is sound not only as a design declaration—that the type is a leaf in the inheritance tree—but also as an optimization proof for the compiler.
Do not expect “I added final and the app became faster.” Call overhead is measured in nanoseconds, so it is usually imperceptible outside hot loops.
The real value lies in the optimizations chained after inlining, which the compiler handles automatically. Our job is simply not to block the proof.
The protocol extension trap — requirement or not?
Dispatch knowledge matters most in practice—more precisely, it hurts most when misunderstood—when dealing with protocol default implementations. Here is a quiz.
protocol Greeter {
func hello() // Declared as a requirement
}
extension Greeter {
func hello() { print("Hello") }
func bye() { print("Goodbye") } // Not a requirement
}
struct Korean: Greeter {
func hello() { print("Hi") }
func bye() { print("Bye") }
}
let k: any Greeter = Korean()
k.hello() // ?
k.bye() // ?
The answer is “Hello” and “Goodbye.” hello is a protocol requirement, so it is dynamically dispatched through the witness table, where the Korean implementation is registered.
bye, by contrast, is an extension-only method not listed as a requirement. Called through the protocol type, it statically jumps directly to the extension implementation, regardless of what Korean defines.
Without this rule, you get the mystery bug: “I clearly implemented it, but my code is not called.” The fix is simple.
Any method adopters must be able to replace must be declared as a requirement in the protocol body. Keep the default implementation in the extension, but the body declaration creates a slot in the table.
In the POP (protocol-oriented programming) article, protocol extensions were introduced as an alternative to inheritance. This is the safety rule for that tool.
Verify with instrumentation — use the profiler, not intuition
The dispatch discussion should end with the same warning: this is micro-optimization, and order matters.
First, find the real bottleneck with Instruments’ Time Profiler. Most performance problems come from algorithms (O(n²) loops), unnecessary work (recomputing every frame), or I/O—not dispatch.
The warning against premature optimization remains exactly as discussed in the Knuth article.
If profiling really identifies dynamic dispatch inside a hot loop, the treatment list opens up: make the type final, replace any with some or generics to encourage specialization, or move the protocol boundary outside the loop.
Put differently, this knowledge is usually for understanding design, not optimization.
Why structs are the default (friendly to static dispatch), why some is preferred over any (it enables specialization), and why SwiftUI uses struct views.
The language’s major decisions all grow from this layer, so understanding dispatch lets you read Swift’s design as one coherent picture.
Summary
- Method calls compile as static (direct), table (vtable/witness table), or message (objc_msgSend) dispatch, and flexibility is inversely related to speed.
- final, private, and WMO prove “no overriding,” lowering dynamic calls to static ones and enabling inlining. final is the default practice for classes that will not be subclassed.
- Protocol extension methods dispatch differently depending on whether they are declared as requirements. Methods that must be replaceable must be declared in the protocol body.
- Profile first. The everyday value of dispatch knowledge is not optimization technique, but the ability to read language design.
Next time we go one layer deeper: memory layout—how a struct’s size is determined, why property order changes memory, and the actual size of an any box.
Continue reading
Sources and verification
- Swift Optimization TipsSwift 프로젝트 · Official documentation · Checked August 17, 2026Supports: Static and dynamic dispatch, final, whole-module optimization, and performance characteristics

![Cover image for [Advanced Swift #5] Swift dispatch: why final boosts performance](/assets/images/posts/f974f02c-833c-4ab1-9350-a2e3543e8391/swift-method-dispatch-1.jpg)