13 min read

In this article by Kostiantyn Koval, author of the book, Swift High Performance, we will learn about Swift, its performance and optimization, and how to achieve high performance.

(For more resources related to this topic, see here.)

Swift speed

I could guess you are interested in Swift speed and are probably wondering “How fast can Swift be?” Before we even start learning Swift and discovering all the good things about it, let’s answer this right here and right now.

Let’s take an array of 100,000 random numbers, sort in Swift, Objective-C, and C by using a standard sort function from stdlib (sort for Swift, qsort for C, and compare for Objective-C), and measure how much time would it take.

In order to sort an array with 100,000 integer elements, the following are the timings:

Swift

0.00600 sec

C

0.01396 sec

Objective-C

0.08705 sec

The winner is Swift! Swift is 14.5 times faster that Objective-C and 2.3 times faster than C. In other examples and experiments, C is usually faster than Objective-C and Swift is way faster.

Comparing the speed of functions

You know how functions and methods are implemented and how they work. Let’s compare the performance and speed of global functions and different method types. For our test, we will use a simple add function. Take a look at the following code snippet:

func add(x: Int, y: Int) -> Int {
return x + y
}

class NumOperation {

func addI(x: Int, y: Int) -> Int
class func addC(x: Int, y: Int) -> Int
static func addS(x: Int, y: Int) -> Int
}

class BigNumOperation: NumOperation {

override func addI(x: Int, y: Int) -> Int
override class func addC(x: Int, y: Int) -> Int
}

For the measurement and code analysis, we use a simple loop in which we call those different methods:

measure("addC") {
var result = 0
for i in 0...2000000000 {
   result += NumOperation.addC(i, y: i + 1)
   // result += test different method
}
print(result)
}

Here are the results.

All the methods perform exactly the same. Even more so, their assembly code looks exactly the same, except the name of the function call:

  • Global function: add(10, y: 11)
  • Static: NumOperation.addS(10, y: 11)
  • Class: NumOperation.addC(10, y: 11)
  • Static subclass: BigNumOperation.addS(10, y: 11)
  • Overridden subclass: BigNumOperation.addC(10, y: 11)

Even though the BigNumOperation addC class function overrides the NumOperation addC function when you call it directly, there is no need for a vtable lookup.

The instance method call looks a bit different:

  • Instance:
    let num = NumOperation()
    num.addI(10, y: 11)
  • Subclass overridden instance:
    let bigNum = BigNumOperation()
    bigNum.addI()

One difference is that we need to initialize a class and create an instance of the object. In our example, this is not so expensive an operation because we do it outside the loop and it takes place only once. The loop with the calling instance method looks exactly the same.

As you can see, there is almost no difference in the global function and the static and class methods. The instance method looks a bit different but it doesn’t have any major impact on performance. Also, even though it’s true for simple use cases, there is a difference between them in more complex examples. Let’s take a look at the following code snippet:

let baseNumType = arc4random_uniform(2) == 1 ?  
   BigNumOperation.self : NumOperation.self

for i in 0...loopCount {
   result += baseNumType.addC(i, y: i + 1)
}
print(result)

The only difference we incorporated here is that instead of specifying the NumOperation class type in compile time, we randomly returned it at runtime. And because of this, the Swift compiler doesn’t know what method should be called at compile time—BigNumOperation.addC or NumOperation.addC. This small change has an impact on the generated assembly code and performance.

A summary of the usage of functions and methods

Global functions are the simplest and give the best performance. Too many global functions, however, make the code hard to read and reason.

Static type methods, which can’t be overridden have the same performance as global functions, but they also provide a namespace (its type name), so our code looks clearer and there is no adverse effect on performance.

Class methods, which can be overridden could lead to a decrease in performance, and they should be used when you need class inheritance. In other cases, static methods are preferred.

The instance method operates on the instance of the object. Use instance methods when you need to operate on the data of that instance.

Make methods final when you don’t need to override them. This gives an extra tip for the compiler for optimization, and performance could be increased because of it.

Intelligent code

Because Swift is a static and strongly typed language, it can read, understand, and optimize code very well. It tries to avoid the execution of all unnecessary code. For a better explanation, let’s take a look at this simple example:

class Object {
func nothing() {  
}
}

let object = Object()
object.nothing()
object.nothing()

We create an instance of the Object class and call a nothing method. The nothing method is empty, and calling it does nothing. The Swift compiler understands this and removes those method calls. After this, we have only one line of code:

let object = Object()

The Swift compiler can also remove the objects created that are never used. It reduces memory usage and unnecessary function calls, which also reduces CPU usage. In our example, the object instance is not used after removing the nothing method call and the creation of object can be removed as well. In this way, Swift removes all three lines of code and we end up with no code to execute at all.

Objective-C, in comparison, can’t do this optimization. Because it has a dynamic runtime, the nothing method’s implementation can be changed to do some work at runtime. That’s why Objective-C can’t remove empty method calls.

This optimization might not seem like a big win but let’s take a look at another—a bit more complex—example that uses more memory:

class Object {
let x: Int
let y: Int
let z: Int

init(x: Int) {
   self.x = x
   self.y = x * 2
   self.z = y * 2
}

func nothing() {
}
}

We have added some Int data to our Object class to increase memory usage. Now, the Object instance would use at least 24 bytes (3 * int size; Int uses 4 bytes in the 64 bit architecture). Let’s also try to increase the CPU usage by adding more instructions, using a loop:

for i in 0...1_000_000 {
let object = Object(x: i)
object.nothing()
object.nothing()
}
print("Done")

Integer literals can use the underscore sign (_) to improve readability. So, 1_000_000_000 is the same as 1000000000.

Now, we have 3 million instructions and we would use 24 million bytes (about 24 MB). This is quite a lot for a type of operation that actually doesn’t do anything. As you can see, we don’t use the result of the loop body. For the loop body, Swift does the same optimization as in previous example and we end up with an empty loop:

for i in 0...1_000_000 {
}

The empty loop can be skipped as well. As a result, we have saved 24 MB of memory usage and 3 million method calls.

Dangerous functions

There are some functions and instructions that sometimes don’t provide any value for the application but the Swift compiler can’t skip them because that could have a very negative impact on performance.

Console print

Printing a statement to the console is usually used for debugging purposes. The print and debugPrint instructions aren’t removed from the application in release mode. Let’s explore this code:

for i in 0...1_000_000 {
print(i)
}

The Swift compiler treats print and debugPrint as valid and important instructions that can’t be skipped. Even though this code does nothing, it can’t be optimized, because Swift doesn’t remove the print statement. As a result, we have 1 million unnecessary instructions.

As you can see, even very simple code that uses the print statement could decrease an application’s performance very drastically. The loop with the 1_000_000 print statement takes 5 seconds, and that’s a lot. It’s even worse if you run it in Xcode; it would take up to 50 seconds.

It gets all the more worse if you add a print instruction to the nothing method of an Object class from the previous example:

func nothing() {
print(x + y + z)
}

In that case, a loop in which we create an instance of Object and call nothing can’t be eliminated because of the print instruction. Even though Swift can’t eliminate the execution of that code completely, it does optimization by removing the creation instance of Object and calling the nothing method, and turns it into simple loop operation. The compiled code after optimization looks like this:

// Initial Source Code
for i in 0...1_000 {
let object = Object(x: i)
object.nothing()
object.nothing()
}

// Optimized Code
var x = 0, y = 0, z = 0
for i in 0...1_000_000 {
x = i
y = x * 2
z = y * 2
print(x + y + z)
print(x + y + z)
}

As you can see, this code is far from perfect and has a lot of instructions that actually don’t give us any value. There is a way to improve this code, so the Swift compiler does the same optimization as without print.

Removing print logs

To solve this performance problem, we have to remove the print statements from the code before compiling it. There are different ways of doing this.

Comment out

The first idea is to comment out all print statements of the code in release mode:

//print("A")

This will work but the next time when you want to enable logs, you will need to uncomment that code. This is a very bad and painful practice. But there is a better solution to it.

Commented code is bad practice in general. You should be using a source code version control system, such as Git, instead. In this way, you can safely remove the unnecessary code and find it in the history if you need it later.

Using a build configuration

We can enable print only in debug mode. To do this, we will use a build configuration to conditionally exclude some code. First, we need to add a Swift compiler custom flag. To do this, select a project target and then go to Build Settings | Other Swift Flags. In the Swift Compiler – Custom Flags section and add the –D DEBUG flag for debug mode, like this:

After this, you can use the DEBUG configuration flag to enable code only in debug mode. We will define our own print function. It will generate a print statement only in debug mode. In release mode, this function will be empty, and the Swift compiler will successfully eliminate it:

func D_print(items: Any..., separator: String = " ", terminator:
String = "n") {
#if DEBUG
   print(items, separator: separator, terminator: terminator)
#endif
}

Now, everywhere instead of print, we will use D_print:

func nothing() {
D_print(x + y + z)
}

You can also create a similar D_debugPrint function.

Swift is very smart and does a lot of optimization, but we also have to make our code clear for people to read and for the compiler to optimize.

Using a preprocessor adds complexity to your code. Use it wisely and only in situations when normal if conditions won’t work, for instance, in our D_print example.

Improving speed

There are a few techniques that can simply improve code performance. Let’s proceed directly to the first one.

final

You can create a function and property declaration with the final attribute. Adding the final attribute makes it non-overridable. The subclasses can’t override that method or property. When you make a method non-overridable, there is no need to store it in vtable and the call to that function can be performed directly without any function address lookup in vtable:

class Animal {

final var name: String = ""
final func feed() {
}
}

As you have seen, final methods perform faster than non-final methods. Even such small optimization could improve an application’s performance. It not only improves performance but also makes the code more secure. This way, you prevent a method from being overridden and prevent unexpected and incorrect behavior.

Enabling the Whole Module Optimization setting would achieve very similar optimization results, but it’s better to mark a function and property declaration explicitly as final, which would reduce the compiler’s work and speed up the compilation. The compilation time for big projects with Whole Module Optimization could be up to 5 minutes in Xcode 7.

Inline functions

As you have seen, Swift can do optimization and inline some function calls. This way, there is no performance penalty for calling a function. You can manually enable or disable inline functions with the @inline attribute:

@inline(__always) func someFunc () {
}

@inline(never) func someFunc () {
}

Even though you can manually control inline functions, it’s usually better to leave it to the Swift complier to do this. Depending on the optimization settings, the Swift compiler applies different inlining techniques.

The use-case for @inline(__always) would be very simple one-line functions that you always want to be inline.

Value objects and reference objects

There are many benefits of using immutable value types. Value objects make code not only safer and clearer but also faster. They have better speed and performance than reference objects; here is why.

Memory allocation

A value object can be allocated in the stack memory instead of the heap memory. Reference objects need to be allocated in the heap memory because they can be shared between many owners. Because value objects have only one owner, they can be allocated safely in the stack. Stack memory is way faster than heap memory.

The second advantage is that value objects don’t need reference counting memory management. As they can have only one owner, there is no such thing as reference counting for value objects. With Automatic Reference Counting (ARC) we don’t think much about memory management, and it mostly looks transparent for us. Even though code looks the same when using reference objects and value objects, ARC adds extra retain and release method calls for reference objects.

Avoiding Objective-C

In most cases, Objective-C, with its dynamic runtime, performs slower than Swift. The interoperability between Swift and Objective-C is done so seamlessly that sometimes we may use Objective-C types and its runtime in the Swift code without knowing it.

When you use Objective-C types in Swift code, Swift actually uses the Objective-C runtime for method dispatch. Because of that, Swift can’t do the same optimization as for pure Swift types. Lets take a look at a simple example:

for _ in 0...100 {
   _ = NSObject()
}

Let’s read this code and make some assumptions about how the Swift compiler would optimize it. The NSObject instance is never used in the loop body, so we could eliminate the creation of an object. After that, we will have an empty loop; this can be eliminated as well. So, we remove all of the code from execution, but actually no code gets eliminated. This happens because Objective-C types use dynamic runtime method dispatch, called message sending.

All standard frameworks, such as Foundation and UIKit, are written in Objective-C, and all types such as NSDate, NSURL, UIView, and UITableView use the Objective-C runtime. They do not perform as fast as Swift types, but we get all of these frameworks available for usage in Swift, and this is great.

There is no way to remove the Objective-C dynamic runtime dispatch from Objective-C types in Swift, so the only thing we can do is learn how to use them wisely.

Summary

In this article, we covered many powerful features of Swift related to Swift’s performance and gave some tips on how to solve performance-related issues.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here