Method Dispatch in Swift

Ilichev Danil
7 min readJul 24, 2022

--

Introduction

Every iOS developer, sometimes without realizing it, is faced with method dispatch. Knowledge of how method dispatching works is essential when writing code, as this knowledge will improve the performance of the application, as well as avoid errors related to non-obvious behavior in Swift.

This article will discuss the concept of dispatching, its types, advantages and disadvantages, and small tasks to strengthen knowledge.

What is method dispatching?

Method dispatching is the process of finding the address of instructions to be executed by the CPU when a particular method is called.

In other words, the purpose of method dispatch is for the program to tell the processor where it can find the executable code for a particular method in memory.

Main types of dispatching

Static Dispatch (Direct Dispatch)

Static dispatching, sometimes referred to as direct dispatching, is the fastest dispatching option. The reason for this speed is that static dispatch prohibits methods from being overridden, resulting in only one implementation of each method. In this case, the address of the required instructions is already known at the stage of compiling the program, which allows the system to jump to the necessary instructions without searching. There is even an optimization technique called devirtualization, whereby the compiler tries to make functions static when possible.

Static dispatch

  • Addresses are known at compile time
  • The fastest
  • Imposes restrictions (prohibition of inheritance and method overriding)

Table Dispatch

This method is based on the fact that the class stores a table with pointers to the implementation of methods. This is the default method in Swift for reference types. The reason this kind of dispatch is the default for reference types is is that classes must support inheritance. In table dispatch, each class has a table with function pointers. Each subclass has its own copy of the table, with a different function pointer for each method overridden by the class. New subclass methods are added to the end of this array.

There are rules for creating virtual tables:

  • If the method is extended from a superclass, copy the address of the method to the subclass’s function table.
  • If the method is created in a subclass, add the address of the method to the end of the function table.
  • If the method is overridden, add the address of the new method to the subclass’s function table.

Let’s take a popular example. Let’s create the Parent and Child classes. Child is an inheritor of the Parent class. Each class implements two methods, with the Child class overriding one method of the Parent class:

class Parent {
func method1() {}
func method2() {}
}
class Child: Parent {
override func method2() {}
func method3() {}
}

At the compilation stage, two virtual tables will be created:

When method2 is called on an instance of the Child class, the following algorithm will run:

  • Reading the virtual table of the Child class at address 0xB00
  • Reading the reference at address 0xB00 + 1. In our case, the method index for method2 is 1, so address 0xB00 + 1 is read
  • Following a link

Table dispatch

  • Tables are created at compile time, but the table is accessed at runtime to determine the method to run
  • Ease of implementation
  • Slower than static dispatch

Message Dispatch

Message dispatching is the most dynamic, but also the slowest, although this disadvantage is partially offset by the caching mechanism, which makes search almost as fast as in the case of table dispatching. Swift usually does not use this type of dispatching, as it’s an Objective-C concept. To force a change in dispatch, you must use the dynamic modifier. Consider an example:

class Parent {
dynamic func method1() {}
dynamic func method2() {}
}
class Child: Parent {
override func method2() {}
dynamic func method3() {}
}

In this case, the Child class holds a reference to super (Parent). In order for the program to understand which method needs to be executed, it is necessary to pass through the entire hierarchy. This is a time-consuming operation, but after going through the hierarchy once, the next time the speed of finding the necessary instructions will be comparable to table dispatch due to the caching mechanism.

Message dispatching provides developers with additional tools. The swizzling method allows you to replace the method right at runtime, while leaving the original implementation available, and isa-swizzling allows you to change the type of an object.

Message dispatch

  • Read class hierarchy at run time to determine which method to call
  • More dynamic than table dispatch
  • Can change method behavior and object type at runtime
  • Slower than table dispatch

Choosing dispatching type and their modifiers

The choice of dispatching option is influenced by the type of object and the place of the method declaration. Swift has a number of modifiers that change the type of dispatching.

final

final enables static dispatch. It is enabled by default unless the method is overridden or the class is not inherited. This modifier disables overriding and inheritance, and hides the method from the Objective-C runtime. But you need to take into account that final only enables static dispatch where possible, but does not disable table dispatch, for example: final override.

dynamic

dynamic enables message dispatch, but does not expose visibility to the Objective-C runtime.

@objc / @nonobjc

@objc and @nonobjc change the visibility for the Objective-C runtime. @nonobjc turns off visibility and is the default for optimizations by disabling message dispatching. @objc, on the other hand, enables visibility to the Objective-C runtime.

General rules are given below:

Performance Impact

Static dispatching is less expensive than dynamic dispatching. To improve performance, the task of the compiler and developer is to ensure that as many methods as possible use static dispatch, the following keywords will help us with this:

  • final does not allow classes to be inherited, and methods to be overridden, which leads to static dispatch
  • private restricts the visibility of a method or the entire class. The absence of any overrides allows the compiler to automatically add final keyword
  • Whole Module Optimizationallows the compiler to view all source files in a single module. This allows the compiler to use finalfor all methods without overriding.

This Apple article goes into more detail about ways to improve performance by reducing the use of table dispatch.

Tasks

For a better understanding of the topic, consider three tasks:

  1. What is displayed when running the code and what type of dispatching are used when calling methods?
protocol Animals { }

extension Animals {
func method() {
print("🐶")
}
}

struct Pets: Animals {
func method() {
print("🐈")
}
}

let firstAnimal = Pets()
firstAnimal.method()

let secondAnimal: Animals = Pets()
secondAnimal.method()

Answer:
After executing the line firstAnimal.method() it will display “🐈”, after executing secondAnimal.method() it will display ”🐶”.
firstAnimal: method declared inside structure — static dispatch
secondAnimal: method declared in protocol extension — static dispatch

2. What type of dispatching are used when calling methods?

protocol Animals {
func method()
}

struct Pets: Animals {
func method() {
print("🐈")
}
}

let firstAnimal = Pets()
firstAnimal.method()

let secondAnimal: Animals = Pets()
secondAnimal.method()

Answer:
firstAnimal: method declared inside structure — static dispatch secondAnimal: method declared at protocol declaration — table dispatch

3. Will the code compile? If not, what should be added?

class Animals { }

extension Animals {
func method() { }
}

class Pets: Animals {
override func method() { }
}

Answer:
The compiler will generate the following error: Non @objc instance method 'method()' is declared in extension of 'Animals' and cannot be overridden.
The fix is ​​to add the @objc keyword before the function declaration in the extension, which turns on Objective-C visibility. The compiler allows you to override the extension for Objective-C compatibility, but in fact, this violates the language directive, so you should avoid such constructs.

Conclusion

We’ve explored the main types of method dispatch, discussed the choice of dispatch options and their modifiers, and looked at ways to improve performance by using static dispatch. The following tips will help you write more performant code:

  • Use value type instead of reference type whenever possible
  • Use extensions to declare methods whenever possible
  • Use final keyword for classes and methods unless the class is inherited and the method is not overridden
  • Use private keyword to limit the scope
  • If possible, do not use dynamic keyword

Additional sources

--

--