Method Dispatch in Swift
Method Dispatch is how a program chooses which instructions to execute when Method is called. This is something that happens every time a method is called, but we probably don’t pay much attention to it. When you want to write efficient code, it’s important to understand how Method Dispatch works. It also helps to explain a lot of Swift’s seemingly inexplicable behaviour.
Types of Dispatch
The goal of Dispatch is to let the program tell the CPU where in memory it can find the executable code for a method call. Before we delve into how Swift does it, let’s take a look at three different ways of sending messages. Each method of message dispatch involves trade-offs and compromises between execution efficiency and dynamism.
Direct Dispatch
Direct Dispatch is the fastest method of Dispatch. Not only does it produce less assembly code, but it also allows the compiler to do all sorts of optimizations, like Inlining code and many more things that are beyond the scope of this article. This method is also often referred to as static dispatch.
However, direct distribution is also the most limited from a programming point of view, and is simply not dynamic enough to support inheritance.
Table Dispatch
Table Dispatch is the most common dynamic implementation for compiled languages. The table dispatches an array of function Pointers to all methods contained in this class that are used in each declared class. Most languages use the term Virtual table, but Swift uses the term Witness table. Each subclass has a copy of the function table, except for the entry function pointer to the overridden method. When subclasses add new methods, those new methods are added to the end of the array. This table is then queried when the program runs to determine the instructions to be executed by the method call.
Table queries are simple, easy to implement, and the performance characteristics are predictable. However, this approach is still slow compared with direct distribution. From the bytecode generation point of view, there are still two completely redundant reads and jumps, adding some overhead and complexity. Also, it’s thought to be slow because the compiler can’t do any optimizations based on what happens inside the function.
One drawback of this array-based implementation is that extensions do not extend party publications. Because subclasses add new methods to the end of a party table, there is no safe index for extensions to add function Pointers to a party table. The Swift-Evolution Post illustrates these limitations in more detail.
Message Dispatch
Message Dispatch is the most dynamic invocation available. It is the cornerstone of Cocoa framework development and the underlying mechanism for implementing features such as KVO, UIAppearance, and Core Data. A key part of this feature is that it allows developers to edit method dispatch behavior at run time. Not only can method calls be changed by swizzling (method substitution), but individual instance-based distribution can also be allowed.
When a message is dispatched, the runtime traverses the entire class hierarchy to determine which method is invoked. If this sounds slow, it is! However, this lookup method is ensured by an accelerated buffer layer, which is basically as fast as table dispatch once its buffer is warmed up.
Swift Method Dispatch
So what does the Swift method look like when it’s distributed? I don’t have a simple answer to this question, but there are four factors that influence distribution choices:
- Declaration Location
- Reference Type
- Specified Behavior
- Visibility Optimizations
Before I define any of this, it is important to point out that Swift has no documentation specifying when to use table distribution or when to use message distribution. The only certainty is that the dynamic keyword will enable message dispatch through the Objective-C runtime. Everything I mention below is based on my observations of Swift 3.0 behavior and may change in future releases.
Location Matters
Swift has two places to make method declarations: inside the initial declaration of a type, and inside the extension. Depending on the type declaration, this will change the execution of the distribution.
class MyClass {
func mainMethod() {}
}
extension MyClass {
func extensionMethod() {}
}
Copy the code
In the example above, mainMethod will use table distribution and extensionMethod will use direct distribution. I was surprised when I found out. It is not clear, nor intuitive, that these methods behave so differently.