Since it is an extremely young language, Swift has plenty of weird performance quirks. If you come from an Objective-C background, you’ll likely be frustrated that your Swift compile times are not swift at all! When we realized that our compile times were about 2 or 3 times slower than the equivalent code in Objective-C, we decided to investigate.
Here are a few easy conventions we’ve discovered that have awesome improvements on performance.

String formatting

When we first started Swift, we were super excited that we could easily concatenate Strings and other objects into a String using the + operator.
Let’s say we have a Swift file with one function petString that returns a String containing the age of our dog.


func petString(#dogAge: Int) -> String {
    return "My dog is " + String(dogAge) + " years old" 
}

The file takes about 80ms on average to compile from terminal.
However, what if we were to instead use string format:


func petString(#dogAge: Int) -> String {
    return "My dog is \(dogAge) years old" 
}

The compile time, on average, is about 90ms.
We’re done, right? Concatenating is quicker. Wrong.
Let’s now add two more ages to the mix, catAge and birdAge. Our concatenation function is now:


func petString(#dogAge: Int, #catAge: Int, #birdAge) -> String 
{ 
    return "My dog is " + String(dogAge) + " years old, my cat is " + String(catAge) + "years old and my bird is " + String(birdAge) + " years old" 
}

Our format function is now:


func petString(#dogAge: Int, #catAge: Int, #birdAge) -> String 
{ 
    return "My dog is \(dogAge) years old, my cat is \(catAge) years old and my bird is \(birdAge) years old" 
}

The compile time for the concatenation function is now 2000ms. The compile time for the format function is now 92ms.
WOAH. Simply adding two variables causes the concatenation function to become 10x slower. The format function has basically no change.
When you have to combine Strings and non-Strings into a String, always use format. However, if you’re only using Strings, we’ve found that concatenation is actually much quicker than format.

Testing multiple cases

To hammer home this point, let’s add a fourth variable fishAge to our concatenation function.


func petString(#dogAge: Int, #catAge: Int, #birdAge) -> String 
{ 
    return "My dog is " + String(dogAge) + " years old, my cat is " + String(catAge) + "years old, my bird is " + String(birdAge) + " years old and my fish is " + String(fishAge) + " years old" 
}

If you try to compile, you’ll likely receive the error:

error: expression was too complex to be solved in reasonable time; consider breaking up the expression into distinct sub-expressions

Yikes. That’s enough of a reason to stay away from casting to String.
Let’s say we have an enum Food with an isVegetable method.


enum Food { 
    case beef 
    case broccoli 
    case chicken 
    case greenPepper 
    case lettuce 
    case onion 
    case redPepper 
    case spinach 
    func isVegetable() -> Bool { 
        return self == .broccoli || 
            self == .greenPepper || 
            self == .lettuce || 
            self == .onion || 
            self == .redPepper || 
            self == .spinach 
    } 
}

The compile time for this file is about 63ms.
What happens if we change the method to a switch statement?


func isVegetable() -> Bool { 
    switch self { 
        case .broccoli, .greenPepper, .lettuce, .onion, .redPepper, .spinach: 
            return true 
        case .beef, .chicken: 
            return false 
     } 
}

The compile time is now 67ms. So does that mean that using || is quicker? Let’s push the limit.

Our first function is now:


func isVegetable() -> Bool { 
    return self == .broccoli || 
        self == .greenPepper || 
        self == .lettuce || 
        self == .onion || 
        self == .redPepper || 
        self == .spinach || 
        self == .artichoke || 
        self == .cabbage || 
        self == .celery || 
        self == .kale || 
        self == .radish || 
        self == .squash || 
        self == .parsley || 
        self == .yellowPepper 
}

Our second function is now:

func isVegetable() -> Bool { 
    switch self { 
    case .broccoli, .greenPepper, .lettuce, .onion, .redPepper, .spinach, .artichoke, .cabbage, .celery, .kale, .radish, .squash, .parsley, .yellowPepper: 
        return true 
    case .beef, .chicken: 
        return false 
    } 
}

Using ||, we now have a compile time of 260ms. Using the switch statement, we still have a compile time of 67ms.

Always use switch statements when testing more than a few cases. The Swift compiler doesn’t handle multiple || or && very well and increases the compile time exponentially. Plus, switch statements are exhaustive so you’ll never miss a case.

Nil Coalescing Operator

The compiler certainly didn’t like the first approach here. After unwrapping the two views, the build time was reduced by 99.4%.


// Build time: 5238.3ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

// Build time: 32.4ms
var padding: CGFloat = 22
if let rightView = rightView {
    padding += rightView.bounds.width
}

if let leftView = leftView {
    padding += leftView.bounds.width
}
return CGSizeMake(size.width + padding, bounds.height)

ArrayOfStuff + [Stuff]

This one goes something like this:


return ArrayOfStuff + [Stuff]
// rather than
ArrayOfStuff.append(stuff)
return ArrayOfStuff

I do this fairly regularly and it has an impact on the required build time every time. The below was the worst one and the build time reduction here was 97.9%.


// Build time: 1250.3ms
let systemOptions = [ 7, 14, 30, -1 ]
let systemNames = (0...2).map{ String(format: localizedFormat, systemOptions[$0]) } + [NSLocalizedString("everything", comment: "")]
// Some code in-between 
labelNames = Array(systemNames[0..< count]) + [systemNames.last!]

// Build time: 25.5ms
let systemOptions = [ 7, 14, 30, -1 ]
var systemNames = systemOptions.dropLast().map{ String(format: localizedFormat, $0) }
systemNames.append(NSLocalizedString("everything", comment: ""))
// Some code in-between
labelNames = Array(systemNames[0..< count])
labelNames.append(systemNames.last!)

Ternary operator

By doing nothing more than replacing the ternary operator with an if else statement, the build time was reduced by 92.9%. If map is replaced with a for loop, it will drop another 75% (but then my eyes would hurt).


// Build time: 239.0ms
let labelNames = type == 0 ? (1...5).map{type0ToString($0)} : (0...2).map{type1ToString($0)}

// Build time: 16.9ms
var labelNames: [String]
if type == 0 {
    labelNames = (1...5).map{type0ToString($0)}
} else {
    labelNames = (0...2).map{type1ToString($0)}
}

Casting CGFloat to CGFloat

Not sure what I was thinking here. The values were already CGFloat and some parentheses were redundant. After cleaning up the mess, the build time dropped by 99.9%.


// Build time: 3431.7 ms
return CGFloat(M_PI) * (CGFloat((hour + hourDelta + CGFloat(minute + minuteDelta) / 60) * 5) - 15) * unit / 180

// Build time: 3.0ms
return CGFloat(M_PI) * ((hour + hourDelta + (minute + minuteDelta) / 60) * 5 - 15) * unit / 180

Round()

This is a really odd one. The below example variables are a mix of local and instance variables. The problem is likely not the rounding itself but a combination of code in the method. Removing the rounding did a massive difference though, 97.6% to be precise.


// Build time: 1433.7ms
let expansion = a — b — c + round(d * 0.66) + e
// Build time: 34.7ms
let expansion = a — b — c + d * 0.66 + e

Conclusion

Whether or not you have a problem with slow build times, it is still useful to build an understanding of what confuses the compiler. I’m sure you’ll find a few surprises yourself.

Alex Goncharov, iOS Lead

iOS Lead

View posts by

Talk to Us