Improving Swift Performance (feat. Method Dispatch)

Dynamic Dispatch & Static Dispatch
DelightRoom's avatar
Oct 18, 2022
Improving Swift Performance (feat. Method Dispatch)

Dynamic Dispatch & Static Dispatch

Photo by Robin Pierre on Unsplash

Hello, I'm Lidi, an iOS engineer at DelightRoom. Today, I'm here with a topic called Improving Swift Performance. But before we get to the main point! There is a concept we need to know first. It's none other than Method Dispatch.

For those wondering, "Huh? Dispatch? Like DispatchQueue?" I'll briefly explain what it is before we dive deep into it.

Method Dispatch

Method Dispatch is the mechanism that helps to decide which operation should be executed, or more specifically, which method implementation should be used. Reference

Method Dispatch is a mechanism that helps determine which operation will be executed, and specifically, which function implementation should be used.

"Huh? What does that mean..."

Let's look at the dictionary definition of "dispatch" first!

dispatch: [verb] to send off to a destination or for a purpose.

In other words, Method Dispatch is about the timing of delivering the method to be executed. It's a story of "at what point the function to be executed is finalized and dispatched."

"Doesn't that just happen automatically? What's the advantage of knowing this?"

I imagine some of you might be asking these questions. This timing of method dispatch is often determined automatically, but it can also be explicitly decided by the engineer. And these decisions directly impact runtime performance.

Method Dispatch Type

There are two types of Method Dispatch: Dynamic Dispatch and Static Dispatch.

Dynamic is literally dynamic, and Static is static. So, how are these two different? Let me briefly summarize their characteristics:

  • Dynamic dispatch → Supports inheritance and override

  • Static dispatch → Does not support inheritance and override

I think you might be getting the hang of it now. Let me explain a bit more using Swift, which is familiar to us.

Dynamic Dispatch & Static Dispatch

By default, Swift uses the Dynamic Dispatch method.

Like many object-oriented programming (OOP) languages, this is because of polymorphism. Thanks to it, an inherited class can override the parent class's functions, or just borrow and use them as is. This enables versatile expressiveness and scalable programming. But if you gain something, you're bound to lose something else! The classic law of equivalent exchange...!

What we lose in return is exactly Runtime Performance... (We've lost something way too huge... ㅠㅠ)

Then why is Dynamic Dispatch slower than Static Dispatch? The secret lies in the vtable.

vtable(virtual table)

In Swift, dynamic dispatch defaults to indirect invocation through a vtable. If one attaches the dynamic keyword to the declaration, Swift will emit calls via Objective-C message send instead. In both cases this is slower than a direct function call because it prevents many compiler optimizations in addition to the overhead of performing the indirect call itself. Reference

As the description points out, Dynamic dispatch in Swift defaults to indirect invocation via a vtable. And it states that this is slower compared to direct calls.

So what exactly is a vtable? Namuwiki explains the vtable like this:

Whenever a class defines a virtual function (or virtual method), most compilers add a hidden member variable to the class, which points to an array of pointers to (virtual) functions (called a Virtual Method Table (VMT or Vtable)). These pointers point to the exact functions during execution time because at compile time, it is not known whether the base function will be called or if it will be implemented by a class inheriting from the base class. Reference

Does that make sense? Let me break it down into simpler terms.

We can override and inherit through Swift's Dynamic Dispatch (whether we actually do it or not doesn't matter), and for this purpose, methods and properties are stored in an array in the vtable. Then, at the moment a call occurs (Runtime), it finds the corresponding methods and properties in the Method Table and invokes them via an indirect call.

This approach is naturally slower than direct call methods and negatively impacts compiler optimization.

So, we can logically deduce that avoiding Dynamic Dispatch, except when polymorphism is strictly necessary, will have a positive impact on performance.

Now! Let's take a quick quiz to see how much our understanding has improved.

  1. Is a class static? / dynamic?

  2. Is a method static? / dynamic?

  3. Is a protocol static? / dynamic?

  4. If you extract a class into an extension, is it static? / dynamic?

Shall we take a brief moment to think before scrolling down?

.

.

.

Answers:

  1. Classes are Reference Types and can be inherited. Therefore, they use a vtable. So the answer is dynamic.

  2. Since methods are also Reference Types, they all inherently hold memory addresses as pointers. So the answer is dynamic!

  3. Because it needs to verify where it is being adopted during runtime, it uses Dynamic.

  4. Unless you borrow the power of @objc, overriding is impossible. So it's static!

Then let's learn how to boost performance through today's main topic: Reducing Dynamic Dispatch!

Reducing Dynamic Dispatch

Apple recommends reducing Dynamic Dispatch through the following methods. Reference

final

Use final when you know that a declaration does not need to be overridden

Using the final keyword, we can declare that a specific class no longer requires inheritance, or overriding via inheritance. In other words, you are explicitly telling the compiler that "Dynamic Dispatch is not needed," and through this, the compiler can safely eliminate Dynamic Dispatch.

In the example below, update() is called via Dynamic Dispatch, allowing subclasses to redefine it through override. However, by adding final, point and velocity are accessed directly via the object's stored properties, and it changes to a direct call to updatePoint(). Naturally, overhead decreases and performance improves.

class ParticleModel {
    final var point = ( x: 0.0, y: 0.0 )
    final var velocity = 100.0

    final func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
	point = newPoint
	velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
	updatePoint(newP, newVelocity: newV)
    }
}

You can also apply it to the entire class by attaching final to the class itself. When you do this, all properties and methods implemented within the class are converted to direct calls, and overriding becomes impossible.

final class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0
    // ...
}

private

Infer final on declarations referenced in one file by applying the private keyword.

The second method uses access control modifiers. The private keyword can restrict where something can be referenced to the current file. Therefore, the compiler evaluates whether the private keyword can potentially be overridden wherever it is referenced. And if there is nowhere that overrides it, the compiler automatically infers the final keyword and can remove indirect calls for method and property accesses.

class ParticleModel {
    private var point = ( x: 0.0, y: 0.0 )
    private var velocity = 100.0

    private func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
        point = newPoint
        velocity = newVelocity
    }

    func update(newP: (Double, Double), newV: Double) {
	updatePoint(newP, newVelocity: newV)
    }
}

Just like the previous example, the one above uses a direct call method for everything except update(), and the example below demonstrates applying it to the entire class by declaring private before the class.

private class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0
    // ...
}

WMO(Whole Module Optimization)

Use Whole Module Optimization to infer final on internal declarations.

By default, the Swift compiler compiles module by module. Because of this, the internal access level (the default access level) is only accessible within the module where it's defined. And it's impossible to check if the internal access level has been overridden across different files.

However, if you use WMO, all modules are compiled at once, allowing it to infer whether internal levels are being overridden; if not, it applies final internally.

public class ParticleModel {
    var point = ( x: 0.0, y: 0.0 )
    var velocity = 100.0

    func updatePoint(newPoint: (Double, Double), newVelocity: Double) {
	point = newPoint
	velocity = newVelocity
    }

    public func update(newP: (Double, Double), newV: Double) {
	updatePoint(newP, newVelocity: newV)
    }
}

var p = ParticleModel()
for i in stride(from: 0.0, through: times, by: 1.0) {
    p.update((i * sin(i), i), newV:i*1000)
}

In the example above, you can see that the public access control modifier is attached. If you enable Whole Module Optimization here, it will automatically infer final for point, velocity, and updatePoint().

One silver lining is that since Xcode 8, Whole Module Optimization is turned on by default for release builds. That's one less thing to worry about 🙂

Conclusion

Today, we took a slightly deeper dive into the vague advice we often hear: "add final" and "add access control modifiers." Now, rather than just explicitly communicating intent to our teammates, we can write code with a real understanding of how it benefits app performance.

How was it? I always find it so fun when I discover that seemingly simple things can actually affect app performance! I hope you all felt that same sense of fun while reading this post :)

I'll be back every now and then with more fun engineering stories~!

Thanks for reading this long post 👨‍💻

👉 Check out our Instagram for a sneak peek into DelightRoom's daily life

Share article