Apple has made considerable claims about how fast the Swift programming language is supposed to be, stating e.g. that it is up to 2.6x faster compared to Objective-C.
The details of tests behind those claims were never disclosed and those who verified them themselves found quite opposite results – Swift can be by an order of magnitude slower compared to Objective-C when the code is non-optimized. With optimizations, it is still usually slower, but the difference is not as great.
In his book, iOS and macOS Performance Tuning, the software engineer and researcher Marcel Weiher found that there is a way to make Swift run much faster – but one would lose all the convenience Swift brings while going into pure C or C-based functions.
At Cleevio, we strive to write code running as fast as possible while also maintaining convenience, safety, and readability. Swift is therefore great for most of our use cases (although we also made a C-library for a computation-intensive task for one of our projects), as it meets our needs without having to go pure C-blast.
There are, however, many ways to tackle certain code problems, some more and some less performant. This article focuses on the difference between static and dynamic dispatch. We'll look at it in code and cover the performance implications of its use. We will also discuss the limitations in situations when it’s much less convenient to write the code optimally.
Static dispatch describes a situation in which we know what function body will be run in compile time. It’s usually represented by a concrete type or generics.
It could be shown in the following example:
Here, we can be sure that DinnerServer will be the one executing the function and the value we will receive is true. The same applies to classes that are notated with final keyword – they cannot be subclassed, so we are sure during compiling what will be executed.
We can see similar results when we use generics that take advantage of Swift protocols to the limits. Generics are an essential part of making an app with Swift, as the language itself was described to be a protocol-oriented language. Let’s define a protocol first:
Now, we will make a conformance of DinnerServer to this protocol. It is a simple task as the DinnerServer already implements the function.
Let’s say we have a function that takes a Server as an argument and returns the information whether the food is ready. It could be defined like this:
In current implementation, however, we say that it could be any Server. We will get the results, but we don't know what type will do the function and as any Server can be used as variable in the function, we do not even know whether we will receive true. Fortunately, there is a way to tell a compiler what type will be responsible for executing the function using generics.
Here we provide T: Server to the function. This syntax in angle brackets means that we do not expect any Server, but one in particular. The compiler would then create separate versions for every type that we provide in the function in our codebase. So, this will be a very slow margin increase in the size of the compiled application but we can be sure that the execution will be fast, as the compiler would create for us a function that looks like the following:
We can’t see such a function in our codebase, but I present it to you so that you have some mental image of how the compiler can benefit from generics behind the scenes.
We would of course be able to do the same – just declare the isFoodReady(server:) function for every type that conforms to the Server protocol, but that would be an unreasonably wasteful use of our time. Writing it ourselves would also be more error-prone as it would be easy to forget to create it.
Also, it seems feasible when we talk about one function that we would need to create. But what about some more complex code? For example, if we change the Server protocol requirements to include also prepareIngredients and finalizePreparation functions
and then extend our DinnerServer so that it still conforms to the Server protocol:
We can then create a special function for preparing food that takes care of calling both functions like this:
As you saw in the implementation, finalizePreparation for DinnerServer is empty, as our server does not need to do anything more than just prepare the ingredients. But some other Server may need it.
We can now use this in a different function (orderFood) that should crash if the food is not ready after it is supposed to be prepared:
As you can see, if we were to create functions for specific types, the amount of code we would have to write would turn exponential
together with the growth of the number of functions or the types using them.
Generics also help the testability of our code as we can create mocking types just for testing purposes without the need of creating specific functions for them to use.
Dynamic dispatch is the exactly opposite situation - we don’t know what function body will execute in the compile time. So, we cannot help the compiler to create the same optimizations as it can when we use generics.
I have already presented how the dynamic dispatch function would look like in our setting. It would be a function that takes any type conforming particular protocol Server:
The result here is that it requires more work during runtime to execute this dynamic isFoodReady. Assembly code from the compiler tells the story. First of all, generic and non-generic functions have differences in their implementation.
As we can see, there is a special call to __swift_project_boxed_opaque_existential_1 in the non-generic function as this is what we have to work with – existentials. Also, in the calling place of those functions, we can see differences.
Calling our generic function is a quite straightforward code:
While when we try to call our generic function, things start to look more messy:
Most notable difference here is that there is an additional call to execute function __swift_destroy_boxed_opaque_existential_1. This additional overhead is what causes the code to run slower. How much slower? That will come in the next section.
The existential is also the name we can see in Swift 5. 6. context that introduced existential any. It’s just a way of Swift that tries to force us to declare any before any protocol type that we want in a function (for example). This approach should make us more aware of using dynamic dispatch. We can expect to get a warning or error in the future versions of Swift if we don’t use the word any in our codebase. The above-mentioned code would look like this:
Just that simple. All it takes is to add the word any.
Another example of dynamic dispatch would be a class that is not defined as final. The compiler cannot be sure what class is provided to the function – is it the class that is defined in the function, or one that inherits from it? When the class is declared as final, inheritance is no longer possible and the compiler can then create other optimizations. The same applies to specific functions or variables. If declared as final, they cannot be overridden and the compiler doesn’t have to use dynamic dispatch.
Why would anyone use dynamic dispatch then? We will look into that in the section titled Limitations where I’ll show you examples where static dispatch may be – at least – inconvenient to use in some situations.
In general, we can expect dynamic dispatch to be slower than the same functions that use static dispatch. Those are indeed the results. The question here would be – at what cost? And the answer differs based on whether we are talking about generics or final classes.
I will present the results of using static versus dynamic dispatch. For the measurements I created a library that takes functions defined in a type called MeasureFunc (containing function and its name) and the number of repetitions.
It is very straightforward – it repeatedly runs all functions and then appends the result to a dictionary, which is then sorted and the results are then handled – the library can print them as well as save the results as CSV, for example. The average time of each function is then calculated as a mean of the runs, using CAMediaCurrentTime which is much faster to create than Date and should therefore be more precise.
I will show you the results in relation to the fastest function – so, for example, if one function would take 1 minute and the other would take 2 minutes, the first function would be shown as index 100% and the second as 200% – as it takes 2 times more time to execute it.
The code was compiled with release optimisations, but we can expect it to be much slower without them.
I use TestingClass function for all tests that I will present to you, which is declared like this:
It conforms to a protocol Testable, that has only one requirement – to support init without parameters. I’ve used UUIDs because they take some time to generate even though they are value types.
Appending Class to an Array
Code reuse is one of the great ideas of modern programming. We don’t want to repeat ourselves and we want to change, if possible, only one line in one place, instead of one line in multiple places.
First example when I tested statically and dynamically dispatched code was with adding our TestingClass to an Array. Yes, it would be faster to add it to a ContiguousArray, but – let’s be honest here - nobody uses that (and even I do it only sometimes).
The code to add to an array is very simple for generic type, as it is declared by taking one item and appending it 100 000 times.
Using a dynamically dispatched function changes the code, only that we use Testable directly instead of T. With Swift 5.6. we would use any Testable.
The results are not surprising – it is much faster with generic function. How much faster? By about 62%.
Appending Class to an Array with Initialisation
Sometimes, we may need to not only append one specific item to an array repeatedly, but also create it based on some data and create a model from it. The initialization itself would therefore take some time. And even though the appending would be much faster (as we said before, by 62%), the total time of the function should be much lower as the init time should be the same.
The functions are very similar to just append to an Array, with the only difference being that we initialize it when we append it. The generic function looks like this:
While dynamically dispatched looks like this:
The results support what we expected, using dynamic dispatch is 7% slower than using generics, because significant time is used on the creation of the class. If it wasn't a class or didn’t have 5 variables, the dynamic dispatch would be relatively slower (but never more than the 62% that we showed in the previous test).
We’re not going to talk about any recursion algorithms (that are usually not even the most performant way to perform tasks). Here, I will show you a very simple code of some class that takes a parameter and then in turn hands over that parameter. The usage of such may be some dependency injection – like in coordinators or possibly in some SwiftUI ViewModel handling.
Recursion using static dispatch looks like the following. As you can see, we start with some item that conforms to protocol Testable and then recurse 5 times (starting from 0).
Dynamic dispatch looks very similar, the only difference is that we declare storage to be Testable instead of Generic T.
Again, static dispatch is much faster, in this case by 67%.
I found only a minute difference when I declared classes as final. But it is definitely a part of Apple’s optimization tips you can find on their Github . I haven’t focused much on benchmarking final classes in tests for this article, but we can see that we can gain a little when we use final classes to improve not only our runtime, but also compilation times.
I recommend this great benchmark from Swift forums done by Jon_Shier that shows that declaring classes as final can bring us around 1% of compilation speed. Not bad, right? And since using generics takes a little longer, we could gain what we lost. At least partially.
Our iOS architecture is based on MVVM-C. We first provide dependencies to Coordinator, which is then responsible for creating ViewModel, ViewController, and then attaching ViewModel to ViewController (on View, depending on whether it is newer app that uses SwiftUI or older one built purely on UIKit).
It was fairly easy to start taking advantage of static dispatch in coordinators – As instead of:
We now use generics declared as:
That code is even shorter as it’s not necessary to create typealias for Dependencies to use later in functions of coordinator, so that whenever we would need to change the dependencies, we would change it only in one place.
One of the tidbits here is that whenever you try to go for this change of taking generics instead of any existential, you would have to change it in all places – or at least start with some root coordinator and then continue down with the flow. To showcase this, let us declare a protocol Developer:
and a type conforming to it:
This code compiles:
But the following doesn’t. It is due to generics expecting a specific type – and the compiler cannot make a specific type out of existential.
But it's not as easy when we work with ViewModels and ViewControllers. If ViewModel were generic, it would have to be specifically declared in ViewController using Generics as well as in ViewModel itself (you can see it in the following example in the angle brackets where we have to duplicate the code). We would then have to declare what protocols ViewModel conforms to in ViewController, which would then be little harder to refactor when needed.
It’s also not possible to extend a class to be a delegate for objective-c API when that class uses generics, as can be seen in the following example where we’re getting the error „Conformance of generic class ViewController<T> to @objc protocol UITableViewDelegate cannot be in an extension). Yes, it would be possible to solve through another object that would serve as that delegate, but that would require significant changes.
Generics can also become highly complex when we want to have default arguments for Generic dependencies. It’s very easily seen as the context of SwiftUI, but applies to any context that uses Generics. To showcase this, let us create a very easy ContainerView that has two attributes – content and its overlay.
So, what if we want to create some default arguments for it? We would have to extend ContainerView with them. Let’s say that we want to have a default argument for content to be Text and for overlay to be EmptyView.
For Content it would look like this. You can see where Content == Textwhere we declare that this is an extension that applies only when the Content is Text. And also in the init, there is Text with a localized string of „Placeholder“). So whenever you would use ContainerView(overlay:) init, it would show the Text with Placeholder string.
And for Overlay to be by default EmptyView like this. The result is that we can now omit the overlay parameter:
But, what if we wanted to have an init that has two default arguments, Text and EmptyView? We would then have to declare another extension like this. This approach allows us to combine previous two extensions and declare the ContainerView only as ContainerView() without the need to specify any argument:
But what if we had some other arguments that we would want to default? As you can see, the complexity would be exponential here. For two default arguments, we need 3 extensions. For 3, we would need 7 arguments. Here, the discussion should also be about the maintenance, whether a pure Generic approach is suitable from the beginning when the API or app architecture is not finished yet, and might be changed significantly in the future.
It’s worth adding that there’s just no way around it in some places if we decide to use generics. Apple uses exactly the same for their SwiftUI elements. While we can’t see the actual implementation, we can see the headers and names of the functions in the documentation.
There are two arguments for SwiftUI.Label – for Title and Icon. And as Apple wants users to be able to use some convenience inits, they also have two additional extensions (as Apple doesn’t have any default parameters here) containing convenience initializers. For example, in the first extension where Title is Text, there are convenience inits to take String, LocalizedStringKey, as well as String for image or SF Image. As you can see, there is no way to initialize a Label with some View but also a String of an image – that would require them to create another extension. The same applies to the case where we would have Title as a String but Icon being some View. Making API is just hard and you either provide all the convenience inits or make your life easier.
These limitations are definitely not finite. Depending on the context, there may be ways to overcome this. If you have any good examples of limitations or how to solve them, please let me know!
Apple has brought many great additions to Swift 5.7. They significantly improved how we use generics and deprecated some of the limitations I mentioned before.
For example, we defined a following function in the first chapter of this article:
In Swift 5.7., all it takes is to write it as following:
The angle-bracket approach that might have frightened potential users of generics is gone – with the use of the same some keyword we know from SwiftUI, now also in a parameter of a function.
I also showed how hard it is to define default arguments for parameters in SwiftUI. Not anymore. For our use case, we needed 4 inits. All it takes in Swift 5.7 is to have one of them.
And most interestingly, it is now possible to use something people from Apple call existential unboxing. What does it mean? We can now use an existential in a generic function. I think it sufficiently illustrates following example:
As you can see, you can have an existential any type that can call a generic function, which – as it always could – then calls an existential function again. This (with the other additions) is the reason why my presentation for team about generics in Swift 5.7. and how to improve our code with what’s to come was called Big Year For Generics.
From Swift 5.7. we can gradually adopt using generics in code, as it is not necessary now to update the whole coordinator hierarchy if we want to start using generics. We can start with just one and continue along the way.
Swift 5.7. therefore greatly improves what we already had, while the previous code still compiles. Just beware of switching to Xcode 14 too soon, it is by no means stable and has serious issues as of the time of writing the article.
So, what to take from this article? Try to use the power of static dispatch whenever possible. Swift is a great language for the coder, although it is very compiler-intensive. And the compiler needs help from us, which we can achieve by using generics, final classes, or even some private and other declarations.
But be careful about over-optimizing. As I showed in the Limitations section, there are situations in which it would require significant refactorings of current code – and may make it harder if we wanted to refactor that code in the future. Always think about feasibility as well, as you may burn a lot of your – or the client’s– time to optimize some code. And even though there are significant performance implications, our phones, computers, and tablets are so powerful nowadays that they may not be noticeable to the user. It’s great to optimize whenever possible, as it may even save some battery life, but don’t overdo it.
Finally, I wanted to share some pieces of practical advice. I believe that your code that uses existentials without any will get you a warning from the Swift compiler in the future – and when it does, think about making it generic instead of inserting any in there. When it comes to classes, you can for example use the following SwiftLint rule for getting a warning if your code contains a class not declared as final (as you can always disable the rule for classes that are to be subclassed). You can of course change it however you like, in our case, we use it only for Managers, Services, and Coordinators.