Swift. Method Dispatch
Here is a quick introduction, along with the full articles list
…the more restrictive code comes to the compiler, the more performant it is at runtime
Method Dispatch is all about telling CPU where in memory it can find executable code for a particular method call
Direct Dispatch
Direct Dispatch is also called Static Dispatch
The compiler does know which executable code should be used
Direct Dispatch is the fastest style of the method dispatch. It requires a minimal set of assembly instructions to get executable code. Moreover, the compiler can apply optimizations, like inlining code
// Inlining codestruct Point {
let x: Int
let y: Int func draw() {
print("x: \(x), y: \(y)")
}
}func draw(point: Point) {
point.draw()
print("point is drawn")
}// ... initial code
let p1 = Point(x: 0, y: 0)
draw(point: p1)// ... after inlining code
let p1 = Point(x: 0, y: 0)
p1.draw()
print("point is drawn")// ... after inlining code
let p1 = Point(x: 0, y: 0)
print("x: \(p1.x), y: \(p1.y)")
print("point is drawn")
However, this is the most restrictive way. It doesn’t allow things like polymorphism
Table Dispatch
Table Dispatch is the most common method of dispatch among compiled languages, such as C#, Java, and Swift
The compiler does not know which executable code should be used for a particular method call. This decision is taken at runtime
class Animal {
func makeNoise() { }
}class Cat: Animal {
override func makeNoise() { print("meow") }
}class Fox: Animal {
override func makeNoise() { print(".. what does the fox say?") }
}let animals: [Animal] = [Cat(), Cat(), Fox(), Fox(), Cat()]for animal in animals { animal.makeNoise() } // polymorphism
Classes have tables, called Virtual Tables. Each table has an array of function pointers to methods of the corresponding class
Every subclass has its own copy of Virtual Table with different function pointers to those methods, which are overridden. As a subclass adds a new method to its definition, the method is appended to the end of the corresponding table
The compiler builds the tables, and at runtime, the tables are used to determine which method should be called
class ParentClass {
func method1() { }
func method2() { }
}class ChildClass: ParentClass {
override func method2() { }
func method3() { }
}
let obj = ChildClass()
obj.method2()
Each class instance has a property type (or isa in Objective-C, each NSObject has this property). The tables are stored in some static memory region and can be taken by this property
When method2 is called, the process will:
- take Virtual Table for the object of 0xB00 (ChildClass) by its property type
- take the function pointer of method2 from the table (the pointer is taken by index 1). Thus, the function pointer is 0x222
- jump to the address 0x222, that contains the executable code
Each method in such tables isn’t aware of self (obj). At runtime self (obj) is passed as a parameter when calling the method
// ... kind of runtime
method2(obj)
Table Dispatch is still good but less efficient than the previous one because it takes three additional steps (take the table, take the function address, jump to that address to get executable code). And also, it is less efficient because the compiler applies no optimizations
Protocol Witness Tables
Virtual Tables are used when using classes and inheritance, and Protocol Witness Tables — when conforming to protocols
The problem is that structures don’t support inheritance because they don’t have Virtual Tables. But conforming to protocols allows them to use Table Dispatch and polymorphism. However, instead of Virtual Tables, they get Protocol Witness Tables
// Polymorphism without classes and inheritanceprotocol Noisable {
func makeNoise()
}struct Cat: Noisable {
func makeNoise() { print("meow") }
}struct Fox: Noisable {
func makeNoise() { print(".. what does the fox say?") }
}let noisers: [Noisable] = [Cat(), Cat(), Fox(), Fox(), Cat()]for noiser in noisers { noiser.makeNoise() } // polymorphism
Each type (value and reference) that conforms to a protocol has its own Protocol Witness Table. The table has pointers to those methods of the type that are required by the protocol
Structures and classes that conform to a protocol can have different sizes. To store them in the same array (like noisers) or in the same type property, or to pass them as an argument to the same function, and also to find corresponding Protocol Witness Table for each of them, Swift uses existential containers
An existential container is a structure, that always has a fixed size (in x64, it is 5 * 64 = 320 bit or five machine words), and consists of three parts: reference to Value Witness Table (VWT), a reference to Protocol Witness Table (PWT), and a value buffer to store an instance itself
The buffer stores an initial instance. It takes three machine words. If the instance doesn’t fit the buffer size (takes more than three machine words or more than 3 * 64 bit), it gets placed on the heap via VWT (see below), and the buffer contains a reference to that instance. But if it does, the buffer stores the value directly (the value is placed on the stack). However, it is only about value types. For reference types, the buffer stores a reference anyway
Value Witness Table is an abstract representation of instance life cycle manipulations. Since different types (value and reference) have different mechanisms of copying, moving, and destroying their values, VWT is used to abstract from the implementation of the current instance and do those manipulations through a single interface. It has the following properties and methods: size (initial instance size), copy, move, destroy
Also, VWT has methods allocate, and deallocate for placing the instance on the heap if it doesn’t fit the value buffer size and removing the instance from the heap accordingly
Protocol Witness Table, again, stores pointers to those methods of the instance type that are required by the protocol
Due to each existential container has a fixed size, no matter what instance type is stored there, the container itself is placed on the stack
Using POP (Protocol-Oriented Programming) and structures are better than using OOP (Object-Oriented Programming) and classes from the performance point of view — when using structures, we can have polymorphism (because of POP), but avoid reference counting and extra heap allocations (because of structures)
Message Dispatch
Message Dispatch is the most dynamic style of the method dispatch. It’s a cornerstone of Cocoa development. It’s used by KVO, CoreData, UIAppearance…
The compiler, in this case, also does not know which executable code should be used for a particular method call
A key point of Message Dispatch is modifying the dispatch behavior at runtime, applying method swizzling (exchanging method invocations at runtime) or isa-swizzling (exchanging an object type at runtime)
Method Swizzling allows exchanging the implementation of two class methods at runtime. This will affect every instance of a modified class, which was or will be created. If you have methodA and methodB, method swizzling allows you to call the implementation of methodB when calling methodA, and vice versa. It could be helpful for setting some default behavior to a class, avoiding inheritance. But Swift can use protocols for these purposes (Objective-C cannot)
Isa-Swizzling allows exchanging the type of a given single object with another type at runtime. If you have two classes: ClassA and ClassB, you can create an instance of ClassA, then at runtime change its type to ClassB (change property isa), then call some method of ClassB, and then switch the type back to ClassA
class ParentClass {
dynamic func method1() { }
dynamic func method2() { }
}class ChildClass: ParentClass {
override func method2() { }
dynamic func method3() { }
}
Swift builds this hierarchy as a tree structure and puts it into some static memory region. When a message is dispatched (method2 is called), the process will:
- take the hierarchy tree
- try to find the function pointer of method2 in ChildClass table
- if the function pointer is found, jump to the address to get executable code
- else go to ParentClass (super of ChildClass) and repeat 2–4
- do this until the function pointer is found or the root class of the hierarchy is reached
For the first method call, it’s slower than Table Dispatch because it goes through the tree to find the corresponding function pointer. But after that, Swift caches the found table, and the second method call will take Table Dispatch time
Swift uses the Swift runtime whenever it’s possible. It allows making optimizations when preparing code for runtime. Thus, at runtime, the code is more efficient. Swift only opts for a dynamic dispatch and Objective-C runtime if it has no other choice
Objective-C uses Message Dispatch in most cases, but developers sometimes can use Direct Dispatch there
Where a method is declared determines what the method dispatch is used
Inheritance from NSObject makes a class “dynamic message dispatchable”, but methods in initial declarations are called via Table Dispatch
Object type determines which type of dispatch will be used on a method call. If the type is a protocol, then look at the protocol row in the table. If it casts the object to its class type or structure type, then look at the class or value type rows in the table
class A {
func doAction() { } // table dispatch
}class B: A {
final override func doAction() { } // static dispatch
}let a: A = A()
a.doAction() // will be called via table dispatchlet b1: A = B()
b1.doAction() // will be called via table dispatchlet b2: B = B()
b2.doAction() // will be called via static dispatch
final
The final keyword enables Static Dispatch on a method defined in a class. This keyword removes the possibility of any dynamic behavior and also hides the method from Objective-C runtime. It doesn’t generate a selector. Applying final to a class denies inheritance from the class and also makes class methods to be called via Static Dispatch. Using this keyword allows the compiler to make some optimizations for increasing performance. It can be applied to classes only
// denies any inheritance from the class
final class Animal {
// makes the method called via static dispatch
final func move() { }
}
dynamic
The dynamic keyword enables Message Dispatch on a method defined in a class. This keyword implicitly marks the method as @objc, which means the method is visible for Objective-C runtime. Swift doesn’t say that dynamic is available for classes only, but structures and enumerations don’t support inheritance. The runtime doesn’t have to figure out which implementation it needs to use. Thus, at runtime for structures and enums dynamic doesn’t work. Using this keyword can make your code less performant
class Animal {
// message dispatch at Objective-C runtime
dynamic func move() { }
}
@objc
The @objc keyword does not alter the method dispatch. It just makes the method visible for Objective-C runtime. The most common use of @objc is making selectors. And also, using @objc allows overriding methods declared in class extensions (by default, it is impossible because of Static Dispatch)
class Animal {
@objc func move() { } // is visible at Objective-C runtime
}
class Animal { }extension Animal {
@objc func move() { }
}class Lion: Animal {
override func move() { }
}
@nonobjc
The @nonobjc keyword does alter the method dispatch. It can be used to disable Message Dispatch and to make the method invisible for Objective-C runtime. It seems there is no difference between @nonobj and final, so using final makes code more clear
@objc final
Keyword @objc final enables Direct Dispatch on a method and registers the method selector at Objective-C runtime. The keyword allows the method to respond when performing a selector or other Objective-C features, along with giving the performance of Direct Dispatch
class Animal {
// direct dispatch, visible for Objective-C runtime
@objc final func move() { }
}
If a property is observed with KVO and that property is upgraded to Direct Dispatch, the code will still compile, but the dynamically generated KVO method won’t be triggered
SR-103 (Swift bug, protocols)
protocol Greetable {
func sayHi()
}extension Greetable {
func sayHi() {
print("from protocol")
}
}func greetings(greeter: Greetable) {
greeter.sayHi()
}class Person: Greetable { }class Employee: Person {
func sayHi() {
print("from employee")
}
}greetings(greeter: Employee())
// from protocol, however Employee has own implementation
Resources
- https://www.rightpoint.com/rplabs/switch-method-dispatch-table
- https://stackoverflow.com/questions/38877465/are-method-swizzling-and-isa-swizzling-the-same-thing/38878119#38878119
- https://medium.com/better-programming/swift-why-you-should-avoid-using-default-implementations-in-protocols-eeffddbed46d
- https://medium.com/@PavloShadov/https-medium-com-pavloshadov-swift-protocols-magic-of-dynamic-static-methods-dispatches-dfe0e0c85509
- https://cocoacasts.com/what-does-the-dynamic-keyword-mean-in-swift-3/
- https://habr.com/ru/post/473798/
- https://habr.com/ru/post/474558/
- https://developer.apple.com/videos/play/wwdc2016/416/
- https://www.youtube.com/watch?v=ctS8FzqcRug