In the previous article I shared some data regarding Swift compiler performance when compared with Objective-C. I noted that Swift builds are significantly slower than comparable Objective-C code. One may be tempted to mitigate this problem by upgrading build hardware to speed things up.

Don't rush it

When LinkedIn embraced Swift and rebuilt its flagship iOS app using (mostly) Swift, I spent a good amount of time profiling the new compiler characteristics. At that time (mid 2015), it was clear that the compiler was CPU-bound (I/O and memory had no visible impact on build speed). However, the build times were determined primarily by the number of cores and threads available to the compiler, more so than by the CPU clock speed. This made sense as LinkedIn app code was big enough, and we modularized it into independent frameworks that could be built in parallel. Swift compiler made a good use of all the cores available - the more the better!

Based on this analysis, we decided to use 12-core MacPros for core development, and we saw a significant (2-3 times) speedup of our builds compared with quad-core laptops.

Lately, we noticed that majority of our mobile engineers stopped using their expensive MacPro machines, and would spend more time coding and building on their MacBookPro laptops instead. When asked why, devs response was simple: the laptops are faster to build - to the extent where they started to refer to the MacPro as the "trashcan" - not only because of the way they looked.

Being mindful about our expenses, this triggered another investigation. On the surface, the statement made no sense, both in the context of our prior measurements, but also when looking at Mac performance benchmarks available online. In multi-threaded environment MacPro is the most performant piece of Mac hardware available on the market. Even a 7 years old MacPros perform better than the latest quad-core iMacs. So what happened?

Profiling

"xcodebuild" tool allows to control the number of threads used during the builds using "-jobs" option, or with "IDEBuildOperationMaxNumberOfConcurrentCompileTasks" Xcode setting. By default it uses the max available, which equals to the number of virtual cores on the system. For a quad-core MacBookPro, Xcode will use 8 threads. On a 12-core MacPro, it'll be 24 threads. Of course, the number of concurrent build operations kicked off will depend on your project structure and dependency graph.

I measured build times for our main application using different values for the number of concurrent build operations allowed, and discovered a very disturbing phenomenon.

The more cores you let Xcode use, the slower the builds get.

The chart above shows debug build times in seconds (vertical axis) over the number of concurrent jobs allowed (horizontal axis), for 4 different machines I tested: MacBookPro (quad-core late 2013 model, 2.6 GHz i7), MacPro (12-core late 2013 model, 2.7 GHz Xeon E5), iMac (quad-core late 2015, 4 GHz i7) and a MacMini (dual-core late 2014, 3 GHz i7). Builds were performed on OS 10.11.6 (El Capitan) using Xcode 8.2.1.

As you can see, 12-core MacPro is indeed the slowest machine to build our code with Swift, and going from the default 24 jobs setting down to only 5 threads improves compilation time by 23%. Due to this, even a 2-core MacMini ($1,399.00) builds faster than the 12-cores MacPro ($6,999.00).

I re-run the tests for a release flavor, with a smaller app, tried building on macSierra and even built for the new APFS RAM disk suspecting some filesystem I/O congestion issues - with similar results. In the end, the regression in behavior seems to be coming either from the compiler itself, or the OS.

Conclusion and Tips

Until Apple fixes this issue, I suggest you measure and make careful decisions about your build hardware. Your mileage may vary depending on the nature of your code base, mix of Swift, Objective-C, C/C++ and other languages - just don't assume that industry-blessed performance benchmarks apply to the world of Swift.

For machines with more than 4 cores consider reducing number of concurrent jobs allowed for Xcode - until the concurrency issue described above has been resolved. I found 5 to be a good number for MacPro, and 6-8 for MacBookPro and iMac. Depending on the machine and the project, you can get over 20% build time improvement by doing so. To force Xcode to use up to 5 jobs you can run the line below in the Terminal: "defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 5"

by doing so. To force Xcode to use up to 5 jobs you can run the line below in the Terminal: "defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 5" Disable Spotlight indexing for build cache folders. I saw 10% improvement after blacklisting derived data, cache and related folders ("~/Library/Developer" and "~/Library/Caches") from Spotlight indexing.



