Speeding Up Xcode Builds

Ricardo Castellanos
9 min readDec 13, 2019

--

A few suggestions you can use to speed your Xcode build times

After 10 years working as a mobile developer, I have started working in a banking application. It is an application that involves several teams from several companies. There is a lot of functionality. Being such a big project, one of the big problems we encounter every day is the building time.

As projects grow, build times can become problematic. We should feel frustrated while waiting to compile the code to check our changes. However, there are several tweaks you can make to Xcode that can decrease the amount of time it takes for builds to complete without any extra work.

Reducing the build time seemed like an interesting problem to tackle and a good opportunity to learn a bit about iOS internals. Also, there are some tools and settings changes you can use to speed up your Swift compile and build times.

Updated

Mainly there are a couple of approaches to reduce the build time:

  1. Xcode configuration
  2. Appropriate build settings
  3. Code optimization

1. XCode configuration

1.1. Enable build duration setting in Xcode

It’s important to measure build duration when you test these tricks and techniques to see that the changes suggested in this article are actually having the desired effect.

You can enable a timer right within Xcode’s UI. This timer is not visible by default but if you run the following in the command line a time will be displayed each time you build your app.

After you have enabled the timer, you will see the time it takes to compile your app in the build status bar in Xcode. Go to the terminal and write:

$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES

After you have enabled the timer, you will see the time it takes to compile your app in the build status bar in Xcode.

1.2. Use New Build System

Apple launched a new build system in Xcode 9 but it was not activated by default. Apple’s “New Build System” is written completely in Swift and was designed for overall performance and dependency management improvements. However, with Xcode 10, the new build setting has been activated by default and enabled from Xcode Files-> Project/Workspace Settings

You can enable it in your Workspace Settings or by invoking:

xcodebuild -UseNewBuildSystem=YES.

1.3. Add warnings to see if a function or expression are causing longer compile times

Xcode has inbuilt features that allow you to identify functions and expressions that are causing longer compile times. You can specify a compile-time limit and identify areas in your codebase that exceed this limit.

Add below lines in the project build settings ‘Other Swift Flags’

-Xfrontend -warn-long-function-bodies=300
-Xfrontend -warn-long-expression-type-checking=300

The 300 integer represents the compile-time limit you place on your functions and expressions. It is measured in milliseconds.

These flags will warn you if the function or expression is taking more time than you have specified. That means you have to optimize your function or expression.

1.4. Increasing the number of Xcode threads

By default, Xcode will use the same number of a thread as cores our CPU has. Increasing the number of threads Xcode uses can provide a significant performance boost for compiles. This takes advantage of some processors’ ability to multi-thread or simulate additional cores. Keep in mind that you may need to experiment to determine if there are diminishing returns for parallelized builds with your codebase, and then adjust the thread count accordingly. Let’s try configuring Xcode to use 3, 4, or 8 threads and see which one provides the best performance for your use case.
You can set the number of processes Xcode uses from Terminal as follows:

$defaults write com.apple.Xcode PBXNumberOfParallelBuildSubtasks 4

1.5. Increasing the number of concurrent build tasks that are run for Swift projects

In Xcode 9.2, Apple introduced an experimental feature that allows Xcode to run Swift build tasks in parallel. By default, this is not enabled and you will need to switch it on yourself from the command line.

defaults write com.apple.dt.Xcode BuildSystemScheduleInherentlyParallelCommandsExclusively -bool YES

1.6. Remove the ‘Run Script Phase’

Remove the ‘Run Script Phase’ if you have any. Like SwiftLint, Twine, etc.

1.7. Tweak the iOS simulator

The Apple iOS test simulator lets you test across different software and hardware combinations. By using Physical Size or Pixel Accurate window sizes, you can reduce both the size of your tests and the time it takes for them to complete. Ultimately, these configuration changes use fewer resources and help prevent tests from slowing down while simulating pixel-perfect iPhone readers that no one will ever see.

You can pick & drag any corner of the simulator to resize it and set it according to your requirement. Also, you can press CMD+1, CMD+2 or CMD+3

1.8. Parallelized builds

This option allows Xcode to speed up total build time by building targets that do not depend on each other at the same time. This is a time-saver on projects with many smaller dependencies that can easily be run in parallel.

When opening your project in Xcode 10, build parallelization should already be enabled. To check or change this option, open your scheme editor, select “Build” in the sidebar and make sure “Parallelize Build” is checked at the top.

Xcode Scheme editor build options.

1.9. Build the active architecture only

Your project should only build the active architecture when your Debug configuration is selected. This setting should be active by default but it’s worth checking just in case.

Navigate to Build Active Architecture Only in your project’s build settings. Ensure that Debug is set to Yes and release is set to No.

Ensure Build Active Architecture Only is set to Yes for your debug configuration

2. Appropriate Build Settings

2.1. Optimise dSYM generation

DWARF: is a widely used, standardized debugging data format. DWARF was originally designed along with Executable and Linkable Format (ELF), although it is independent of object file formats.

Debug Symbol (dSYM): by default, debug builds of an application store the debug symbols inside the compiled binary while the release builds of an application store the debug symbols in a companion dSYM file to reduce the binary size.

What’s the difference between DWARF and DWARF with dSYM file?

The difference is that in the case of DWARF with dSYM file your Archive app.xcarchive (for AdHoc distribution) contains also the dSYM file needed for reverse symbolization of your code in crash reports. So if you need it for external analysis of crash reports under archiving your app for distribution you should use DWARF with the dSYM file.

The story is that, at the early stages of OSX, Apple didn’t want to go through the hassle of introducing DWARF support in their linker. They created a separate linker for that purpose (dsymutil), which takes debugging information from object files and puts it in a commonplace: a dSYM bundle.

While dSYM bundles are useful for release builds, they are not needed during development. The debugger can obtain its debug information from the intermediate object files which are still hanging around after building.

In XCode, we can set the “Debug Information Format” of our build to “DWARF” instead of “DWARF with dSYM File”.

Ensure you set your Debug Information Format to always create dSYM files for your Release builds and for your debug builds that aren’t being run on the simulator. You don’t need them to be created when running on the iOS simulator.

dSYM files should not be produced when running on the iOS simulator but should be produced for all other instances

2.2. Whole Module Optimization (WMO)

In Xcode, we have the option to select three optimization levels: None, Fast and Fast, Whole Module Optimization.

Using Whole Module Optimization makes compilation very fast. But choosing Fast or Fast, Whole Module Optimization won’t allow a developer to debug the application.

  • Optimization Level

There are two different sections for Optimization Level

1. Apple LLVM 9.0 Code Generation -> Optimization Level -> Debug
There are 6 optimization level for GCC_OPTIMIZATION_LEVEL.

GCC_OPTIMIZATION_LEVEL =
fast (Fastest and Aggressive Optimization)
s (Fastest and Smallest)
3 (Fastest)
2 (Faster)
1 (Fast)
0 (None)

Important: Don’t play with this setting as Swift doesn’t use that optimization setting.

2. Swift Compiler Code Generation -> Optimization Level -> Debug
There are 3 optimization level for SWIFT_OPTIMIZATION_LEVEL
SWIFT_OPTIMIZATION_LEVEL =
-Onone
-O (Fast Single File Optimization)
-Owholemodule (Fast, Whole Module Optimization)

Using Whole Module Optimization makes compilation very fast. But choosing Fast or Fast, Whole Module Optimization won’t allow a developer to debug the application.

3. Code Optimization

3.1. Tips and Tricks

  1. Downloading the Build Time Analyzer for Xcode, which tells you visually how long each part of your code took to compile.
  2. We can optimize the code which will help us to improve the compile time. We have added the other linker flags -warn-long-function-bodies & -warn-long-expression-type-checking to identify the functions and expressions which are taking too much time to compile now we need to optimize those expressions or functions MANUALLY.
  3. Benchmarking for methods in extension v’s methods in the class itself.
  4. Adding type annotations so that the compiler doesn’t need to infer types.
  5. Avoiding the ternary operator, ?:.
  6. Avoiding the nil coalescing operator by unwrapping things by hand with if let.
  7. Using string interpolation rather than concatenation.

3.2. Third-party dependencies

There are a couple of very popular dependency management techniques/tools: Cocoa Pod, Carthage, Swift Package Manager, git Submodule.

The most common way to handle 3rd-party dependencies in iOS projects is to use CocoaPods. It’s simple to use but is not the best option if you care about build times.

One alternative that you can use is Carthage. It is harder to use than CocoaPods but it will improve your build times.

3.3. Optimize CocoaPods

If you use CocoaPods you can optimize all of your dependencies by adding the following to the end of your Podfile.

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if config.name == 'Debug'
config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-Onone']
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Owholemodule'
end
end
end
end

3.4. Small changes to your code

  1. Use ‘final’ when you know the declaration does not need to be overridden. The final keyword is a restriction on a declaration of a class, a method, or a property such that the declaration cannot be overridden. This implies that the compiler can emit direct function calls instead of indirect calls.
  2. Use ‘private’ and ‘fileprivate’ when the declaration does not need to be accessed outside of the file. Applying the private or fileprivate keywords to a declaration restrict the visibility of the declaration to the file in which it is declared. This allows the compiler to be able to ascertain all other potentially overriding declarations.
  3. Use value types in Array: In Swift, types can be divided into two different categories: value types (structs, enums, tuples) and reference types (classes). A key distinction is that value types cannot be included inside an NSArray. Thus when using value types, the optimizer can remove most of the overhead in Array that is necessary to handle the possibility of the array being backed an NSArray.
  4. Use ContiguousArray with reference types when NSArray bridging is unnecessary. If you need an array of reference types and the array does not need to be bridged to NSArray, use ContiguousArray instead of Array:
class C { ... }
var a: ContiguousArray<C> = [C(...), C(...), ..., C(...)]

Good luck and have fun!

If you have some questions, just reply here. Happy Coding. And finally, if you find this article is helpful please do not forget to share, recommend and clap.

--

--