NEW: Master Swift design patterns with my latest book! >>

How to make Swift compile faster

Paul Hudson    September 11th 2017    @twostraws

I was reading an article by Dejan Atanasov about speeding up Swift compile times, and it contained lots of tips that you will have read about elsewhere previously, including:

  1. Downloading the amazing Build Time Analyzer for Xcode, which tells you visually how long each part of your code took to compile.
  2. Force enabling whole module optimization by setting the SWIFT_WHOLE_MODULE_OPTIMIZATION flag to YES.
  3. Make sure Build Active Architecture Only is set to enabled.
  4. Adding type annotations so that the compiler doesn’t need to infer types.
  5. Avoiding the nil coalescing operator by unwrapping things by hand with if let.
  6. Avoiding the ternary operator, ?:.
  7. Using string interpolation rather than concatenation.

These aren’t new tips, and neither is it a new problem – Swift compilation has always been on par with an asthmatic ant carrying heavy shopping, and remains significantly slower than Objective-C despite the best efforts of many engineers.

This is of course because Swift is a vastly more complicated language: as much as we might love type inference, it’s guaranteed to make the compiler work hard.

Right now, the canonical example of problematic code looks something like this:

let sum = [1, 2, 3].map { String($0) }.flatMap { Int($0) }.reduce(0, +)

That converts an array of integers into an array of strings, then back to an array of integers, and adds them up.

It’s not the kind of code you’d write in anything serious, of course, but it does illustrate a problem: what might seem like fairly trivial work to us actually takes over 11 seconds to compile on a top of the range MacBook Pro.

When I was reading Dejan’s article, I started thinking what lengths I would go to to make Swift compile times faster – what sacrifices would I be willing to make in order to stop my MacBook’s fan sounding like it’s preparing for lift off? Perhaps more importantly, is there a readability cost to ditching some of Swift’s best features?

Changes that don’t affect your code

If you’re finding your Swift project is slow to compile, there are three immediate steps you should take.

First, just quickly check that Build Active Architecture Only is set to YES for debug mode. It is the default setting in Xcode and has been for some time, but if you’re working on an older project you never know – you might as well rule this one out straight away so you can forget about it.

Second, enable whole module optimization (WMO) by setting the SWIFT_WHOLE_MODULE_OPTIMIZATION flag to YES. This is different to enabling it as an optimization flag – you need to ensure the optimization is disabled while you’re debugging otherwise you’ll have more problems than an algebra test.

I tested out WMO in AudioKit, which has about 15,000 lines of Swift code – not large by any means, but certainly enough to make my MacBook take a deep breath before it starts compiling. On that project, a regular debug build took 1 minute and 12 seconds on a quad-core 2.9GHz MacBook Pro, whereas performing the same build with WMO enabled took that time down to 42 seconds. That’s a speed up of over 40% without changing a line of code, which is not to be sniffed at.

Third, if you’re still finding your compile time is best measured in ice ages then it’s time to break out Robert Gummesson’s Build Time Analyzer for Xcode, because that will at least tell you the scale of your problem. After all, there’s no point adding type annotations or removing nil coalescing from methods that compile in a tenth of a millisecond.

Now, hopefully that third step has revealed something useful – a method somewhere in your project that takes longer to compile than is reasonable, like the map/flatMap example from earlier. If that’s the case, it’s time for you to make a choice…

Add type annotations

The Swift compiler can spend a lot of time figuring out the types of data you’re working with. Think about it: it’s possible to write ten methods that vary only by their return type – they can have the same name and the same parameter list. Swift needs to figure out which one you mean by looking at what you do with the result, but the result might instead by fed elsewhere, and that result might be fed elsewhere, and so on.

You can give the Swift compiler a helping hand by annotating some or all of your types. Here’s that slow code snippet from earlier:

let sum = [1, 2, 3].map { String($0) }.flatMap { Int($0) }.reduce(0, +)

That takes over 11 seconds to compile, the vast majority of which is taken up with type inference. Swift has to infer the type of the type of the original array, infer the type coming out of map(), infer the type coming out of flatMap(), then use that to figure out the generic type going into reduce().

Now compare this code:

let sum = [1, 2, 3].map { (num: Int) -> String in String(num) }.flatMap { (str: String) -> Int? in Int(str) }.reduce(0, +)

That removes most of Swift’s type inference: we’re telling it explicitly that map() will read in integers and return strings, and that flatMap() will read in strings and return optional integers. And the difference is huge: that code compiles in about 75ms – about 140x faster than the original one-liner.

So, there's no doubt using explicit types is fast, but at the same time you have to admit it's more than a bit unsightly.

Alternatively: make small changes to your code

Swift has some interesting inconsistencies in the way it performs when compiling code, and you may find that small changes produce big results.

Here’s the slow code snippet once again:

let sum = [1, 2, 3].map { String($0) }.flatMap { Int($0) }.reduce(0, +)

That takes just over 11 seconds to compile. Now consider this piece of code:

let sum = [1, 2, 3].map { String(describing: $0) }.flatMap { Int($0) }.reduce(0, +)

That switches the String initializer from “one that converts integers” to “one that converts anything.” It has the same data in and the same data out, but now it compiles in 480ms – more than 20x faster.

Or how about this code:

let numbers = [1, 2, 3]
let stringNumbers = numbers.map { String($0) }
let intNumbers = stringNumbers.flatMap { Int($0) }
let sum = intNumbers.reduce(0, +)

That uses the same String initializer as before, but breaks the code up into multiple lines – it is quite almost identical, except now we have four constants rather than one.

However, that new code compiles in 71ms – about 150x faster than the original one-liner.

Speed, but at what cost?

I talked about double-checking Build Active Architecture Only and enabling whole module optimization first because they are both unobtrusive changes. That is, Swift loses none of its simplicity and expressiveness if you enable those options, and if they happen to be enough for you then quit while you’re ahead – you get to enjoy Swift in all its glory.

But if they aren’t enough - certainly all large projects, and perhaps some medium-sized ones too – then you need to decide what language sacrifices you’re willing to make. For me, type inference is a big thing: I love it, and rely on it as much as possible to avoid cluttering my code. So, in my projects I’d much rather break one line of code up into four or five if I saw Swift was choking on it. As a bonus, the end result of breaking up code is easier to read too – it’s a double win.

At the same time, the type inference example I gave only declared types inside closures – I didn’t have to spell out the type of the array or the type of the resulting sum – and there’s a strong argument to be made that annotating closure parameters is a great way to make your code more readable. So, it’s selective annotation, focused on wherever the build time analyzer says is struggling the most.

Breaking up code does affect the way you write Swift, but it doesn’t make it any less idiomatic. Selectively providing explicit types is also probably more of a benefit than a cost because it helps others understand your code more readily, but at the same time if you annotate some closures but not others you might just end up with a mess – it feels like a false economy to have code that compiles quickly if it is inconsistently written.

Regardless, for me that’s where the line is drawn: this fast and no further. I like the ternary operator as much as anyone, but I’ll be damned if I start annotating all my types, and quite frankly you’ll have to pry nil coalescing from my cold, dead hands.

Where do you draw the line between natural Swift and fast compiling? What tweaks have you found to make your projects compile faster? Tweet me your views at @twostraws and I’ll publish the best.

 

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Click here to visit the Hacking with Swift store >>