Javier de Martín


Dispatching Methods in Swift

In object-oriented languages, like Swift, method dispatch is the mechanism that tells the CPU where in memory the executable code for a particular method call is located.

While this is not something that’s given much thought everyday it can help understand specific performance issues and gain insights on how the compiler works.

Compiled languages have three primary methods of dispatch: static dispatch, table dispatch and message dispatch.

Static, direct or compile-time dispatch

If a call is statically dispatched the compiler is able to locate at compile time where the instructions for that method are located. The system can jump directly to the memory address of the call to perform the operation.

This behavior results in fast execution while also allowing the compiler to perform different kinds of optimizations like inlining or function devirtualization1.

Value types use this dispatch method and also methods declared on extensions.

Table or dynamic dispatch

Each of the methods declared on a class are associated with a virtual method table. A vtable defines an array of function pointers to the implementation corresponding to that class.

This table is built at compile time as for every class it needs to determine if it has been subclassed so the table can contain which implementation of a class to call.

Message dispatch

This mechanism is provided by Objective-C. Every time an Objective-C method is called the invocation is passed to objc_msgSend which handles the lookups as it starts with the given class and iterates through the class hierarchy to pick the correct implementation. The cost of resolving the appropriate implementation can add some overhead making it a little expensive as the lookup performance is guarded by caching mechanism.

Opposed to table dispatch, the message passing dictionary could be modified at runtime, making the program behavior be adjusted while running. This is known as swizzling.

To be able to avoid Swift’s compiler to perform optimizations that avoid the use of method interception there are a couple of things you can do to enable message dispatch: Extending NSObject or using the objc keyword or adding the dynamic attribute to a function.

Swift Compile Process

To better understand how different types of dispatches are reflected on a program we need to better understand how Swift compilation works at a high level.

  1. Swift code is fed to a Swift Front End that translates the source code into a high-level platform agnostic SIL (Swift Intermediate Language), swiftc --emit-sil
  2. A SIL Optimizer takes the SIL and optimises it on a high-level of abstraction and creates an output in the IR (Intermediate Representation) format, which is low-level yet platform agnostic, swiftc --emit-ir.
  3. Then, IR is optimized (LLVM IR Optimizer).
  4. The last phase receives the optimized IR and uses a Code Generator to generate a assembly code, swiftc -S.

To put all of the above into practice let’s analyse how the different dispatch strategies are manifested on the SIL output for different types in Swift.

As a sample, a vtable has the next format,

sil_vtable A {
  #A.foo: (A) -> () -> () : @$s4main1AC3fooyyF    // A.foo()
  #A.init!allocator: (A.Type) -> () -> A : @$s4main1ACACycfC    // A.__allocating_init()
  #A.deinit!deallocator: @$s4main1ACfD    // A.__deallocating_deinit
}

The vtable above can be defined as a dictionary that maps method names to their implementations so the program knows where in memory a specific method implementation is located.

Let’s see how the selection of different types or keywords affect the SIL output with different code snippets.

Classes

class A {
    func foo() { }
    func bar() { }
}

class B: A {
    override func foo() { }
    func bor() {} 
}

final class C: A {
    override func foo() { }
    func baz() { }
}
sil_vtable A {
  #A.foo: (A) -> () -> () : @$s4main1AC3fooyyF    // A.foo()
  #A.bar: (A) -> () -> () : @$s4main1AC3baryyF    // A.bar()
  #A.init!allocator: (A.Type) -> () -> A : @$s4main1ACACycfC    // A.__allocating_init()
  #A.deinit!deallocator: @$s4main1ACfD    // A.__deallocating_deinit
}

sil_vtable B {
  #A.foo: (A) -> () -> () : @$s4main1BC3fooyyF [override]    // B.foo()
  #A.bar: (A) -> () -> () : @$s4main1AC3baryyF [inherited]    // A.bar()
  #A.init!allocator: (A.Type) -> () -> A : @$s4main1BCACycfC [override]    // B.__allocating_init()
  #B.bor: (B) -> () -> () : @$s4main1BC3boryyF    // B.bor()
  #B.deinit!deallocator: @$s4main1BCfD    // B.__deallocating_deinit
}

sil_vtable C {
  #A.foo: (A) -> () -> () : @$s4main1CC3fooyyF [override]    // C.foo()
  #A.bar: (A) -> () -> () : @$s4main1AC3baryyF [inherited]    // A.bar()
  #A.init!allocator: (A.Type) -> () -> A : @$s4main1CCACycfC [override]    // C.__allocating_init()
  #C.deinit!deallocator: @$s4main1CCfD    // C.__deallocating_deinit
}

Class A defines both foo and bar methods. Both are reflected on the vtable as they can be inherited by a subclass and they have to be referenced at runtime.

A B class inherits from A while only overriding foo and also defining a new bor method. SIL’s table for B reflects the new bor method while also displaying how foo and bar have been defined from A.

Lastly, C has the same behavior described in B for both foo and bar. And as it is marked as final the vtable will not have an entry for C.baz as it cannot be overriden.

Overriding methods in extensions

import Foundation

class A {
    @objc dynamic func foo() { }
    func bar() { }
}

class B: A { }

extension B {
    @objc dynamic override func foo() { }
}

Overriding any of the methods defined in A in a subclass’ extension like B it is not allowed by the compiler. This behavior is not supported as non-objc properties and methods cannot be overriden in extensions.

To be able to make this work methods or properties need to be declared as both @objc and dynamic so the compiler will use message dispatch allowing methods and properties to be overriden in an extension.

sil_vtable A {
  #A.bar: (A) -> () -> () : @$s4main1AC3baryyF	// A.bar()
  #A.init!allocator: (A.Type) -> () -> A : @$s4main1ACACycfC	// A.__allocating_init()
  #A.deinit!deallocator: @$s4main1ACfD	// A.__deallocating_deinit
}

sil_vtable B {
  #A.bar: (A) -> () -> () : @$s4main1AC3baryyF [inherited]	// A.bar()
  #A.init!allocator: (A.Type) -> () -> A : @$s4main1BCACycfC [override]	// B.__allocating_init()
  #B.deinit!deallocator: @$s4main1BCfD	// B.__deallocating_deinit
}

The dynamic keyword enables the possibility of overriding a declaration on an extension and the resulting SIL output is the same as before. It also enforces message dispatch for that call.

Value types

As value types, like structures, do not support inheritance the method dispatch of choice is static dispatch. Definitions of value types are not going to be reflected directly on the vtable of the SIL output.

Protocols

The usage of protocols introduce a new concept visible in the SIL table, a witness table.

protocol Parentable {
    func foo()
}

class A: Parentable {
    func foo() { }
}

struct B: Parentable {
    func foo() { }
}
sil_vtable A {
  #A.foo: (A) -> () -> () 
  #A.init!allocator: (A.Type) -> () -> A
  #A.deinit!deallocator
}

sil_witness_table hidden A: Parentable module main {
  method #Parentable.foo: <Self where Self : Parentable> (Self) -> () -> ()
}

sil_witness_table hidden B: Parentable module main {
  method #Parentable.foo: <Self where Self : Parentable> (Self) -> () -> ()
}

A witness table is emitted for every declared explicit conformance. For this sample two witness tables can be seen on the SIL output: one for the class A and one for the structure B.

Couple of extra things

final keywords enables static dispatch on a method defined in a class. This keyword removes the possibility of any dynamic behavior while also hiding the method from the Objective-C runtime not generating a selector.

dynamic enables message dispatch on a method defined in a class while also making it available to the Objective-C runtime. As seen before, the dynamic keyword can be used to allow methods declared in extensions to be overriden.

@objc & @nonobjc alter how the method is seen by the Objective-C runtime. Generally, @objc is used to namespace the selector and it doesn’t alter the dispatch selection method, it just makes it available to the Objective-C runtime. @nonobjc does alter the dispatch selection as it can be used to disable message dispatch since it does not add the method to the Objective-C runtime which message dispatch relies on.


  1. The compiler decides, at compile time, which function should be called so it can produce a direct (or static) call to that function or even inline it. [return]