Improve Swift Performance Through Access Control

A few years ago, Apple's Swift team posted an interesting article titled Increasing Performance by Reducing Dynamic Dispatch. It's a very interesting read, highlighting some of the more subtle aspects of the Swift language and its compiler.

In today's tutorial, I'd like to zoom in on Swift performance and how it's affected by access control. Access control is a feature that's sometimes overlooked by developers new to the language. The goal of this tutorial is to show you how important it is to think about the code you write and how every line fits into the bigger picture.

A Word About Access Control

Access control in Swift isn't difficult to learn. Access levels and their definitions have evolved a little over the years and I feel we know have a solid solution for implementing access control in Swift.

If you're coming from Objective-C, then it may take a while before you understand the benefits of access control. Effectively using access levels is one of the things that set a junior developer apart from a more experienced developer. Let me show you why that is.

More Than Defining Access Levels

Access control may not seem that useful if you work alone or as part of a small team. It's true that access control really shines if you're developing a framework, library, or SDK that's integrated in other software projects. However, thinking that access control is only useful or necessary if you're working on a codebase that's distributed and used by third parties is a misconception.

Access control has many benefits, some of which are subtle and easily overlooked. An obvious benefit of properly applying access control is communication. Even if you're working in a team of one, it pays dividends to learn and correctly apply access control in your projects. By attaching the private keyword to an instance method of a class, you implicitly communicate that the instance method should't be overridden by subclasses. With one carefully chosen keyword, your code documents itself. Every Swift developer understands why the instance method cannot be overridden if it's declared private.

Improving Swift Performance

There's more, though. Access control has side effects that many aspiring Swift developers aren't aware of. Did you know that the compiler inspects the access levels you've applied to optimize the performance of your code? That's what I want to talk about today.

To understand how access control can result in more performant software, we need to take a detour and talk about method dispatch in Swift. Don't worry, though. I only touch on the basics. It's a technical detour, but I promise you it's an interesting one.

What Is Method Dispatch

When a method is invoked on an object or one of its properties is accessed, that object is sent a message. The runtime needs to figure out which method corresponds with the message. Take a look at this example.

window.makeKeyAndVisible()

We invoke the makeKeyAndVisible() method on the window object, a UIWindow instance. At runtime, a message is sent to the window object. While it may seem obvious to you which method needs to be invoked for the message, that isn't always the case.

What happens if we're dealing with a UIWindow subclass that overrides the makeKeyAndVisible() method? The runtime needs to figure out if it needs to invoke the makeKeyAndVisible() method of the subclass or the superclass.

Method dispatch is the set of rules the runtime utilizes to infer which method to invoke for a particular message. Swift relies on three types of method dispatch, direct dispatch, table dispatch, and message dispatch. Direct dispatch is also referred to as static dispatch. Table and message dispatch are types of dynamic dispatch.

Dynamic Dispatch

Message dispatch is what powers Objective-C and the Objective-C runtime. Every message that's sent is dispatched dynamically. What does that mean? The Objective-C runtime figures out which method to invoke for a message at runtime by inspecting the class hierarchy. That's why Objective-C is such a dynamic language. Objective-C dynamism is also what powers several Cocoa features, including Key-Value Observing and the target-action pattern.

There's one important downside to dynamic dispatch. Because the runtime needs to figure out which method to invoke for a message, dynamic dispatch is slow compared to direct dispatch. In other words, dynamic dispatch comes with a bit of overhead.

Static Dispatch

Static dispatch, also known as direct dispatch, is different. The compiler can infer at compile time which method to invoke for a message. As the name implies, this isn't dynamic. What's lost in flexibility and dynamism is gained in performance.

The runtime doesn't need to figure out which method to invoke at runtime. The small performance hit dynamic dispatch suffers from is absent if static or direct dispatch is used.

Optimizing for Performance

While I won't dig deeper into method dispatch in today's tutorial, I want you to remember that static dispatch is more performant than dynamic dispatch. To improve performance, the compiler's goal is to promote method invocations from dynamic to static dispatch as much as possible.

Optimization Through Access Control

While Objective-C relies exclusively on message dispatch, Swift uses a combination of direct, table, and message dispatch. It favors static dispatch over dynamic dispatch. To keep the discussion focused, I only consider static and dynamic dispatch in the remainder of this tutorial.

Inheritance is a powerful paradigm, but it makes it more difficult for the compiler to figure out which method to invoke for a message. Take a look at this example.

import UIKit

class ViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Fetch Notes
        fetchNotes()
    }

    // MARK: - Helper Methods

    func fetchNotes() {
        ...
    }

}

The ViewController class defines the fetchNotes() method. You probably know that methods and properties are declared internal by default, which means that the method or property is accessible by other entities defined in the same module. Is it sufficient to declare fetchNotes() internal? That depends.

Because we attached the internal keyword to the fetchNotes() method, it's possible for a ViewController subclass to override the fetchNotes() method. The result is that the compiler is unable to figure at compile time which implementation to execute when the fetchNotes() method is invoked. The runtime needs to dynamically dispatch invocations of the fetchNotes() method.

Analyze the Code You Write

When more experienced developers look at the code they write, they consider how it fits into the project they're working on. The implementation of a method is only part of the solution. Should it be possible for ViewController subclasses to override the fetchNotes() method? If the answer is no, then you should attach the private or fileprivate keyword. This not only makes sense in the context of access control, it also improves performance. Why is that?

When the compiler inspects the fetchNotes() method, it realizes that it's declared private, implying that the method cannot be overridden by a subclass. The compiler picks up this clue and safely infers final on the method declaration. Whenever the final keyword is attached to a method declaration, calls to that method can be dispatched statically instead of dynamically, resulting in a tiny performance gain.

Whole Module Optimization

This tutorial wouldn't be complete without a mention of Whole Module Optimization. The Swift compiler is an amazing piece of engineering and it sports a slew of fantastic features we aren't aware of. One of these nifty features is Whole Module Optimization.

Whole Module Optimization is disabled by default for debug builds. This results in shorter compile times, but you pay a price for the time you save. Without Whole Module Optimization each file in your project is compiled separately, not taking the rest of your codebase into account. That's fine during development.

When you build your project for distribution, however, Whole Module Optimization kicks in to optimize your application's performance. The compiler no longer treats each file separately. It builds the puzzle that is your project. What does that mean and why is that important?

Take another look at the code snippet I showed you earlier. Remember that a call to fetchNotes() is dynamically dispatched at runtime. That isn't true if Whole Module Optimization is enabled. When the compiler inspects the entire module, your project, and figures out how each file fits into the bigger picture, it discovers that there are no ViewController subclasses overriding the fetchNotes() method. That means the compiler can infer final on the fetchNotes() method declaration.

The final keyword means that a method or property cannot be overridden in subclasses. The result, as we saw earlier, is that calls to fetchNotes() can be statically dispatched, even if fetchNotes() isn't declared private. That's one smart compiler. Isn't it?

Keep Learning

I often write about growing as a developer and I emphasize how important it is to invest in your education. Learning about the finer details of the Swift language has changed me as a developer. The code I write today is different from the code I wrote a year ago.

While method dispatch may seem like an advanced topic, I feel it's as important as learning about Automatic Reference Counting or protocol-oriented programming. The Swift language is easy to pick up and that's a good thing. If you're serious about becoming a great developer, though, it's important to continue learning and to push the envelope.

Stop Writing Swift That Sucks

Download the Swift Patterns I Swear By