Building Swift Code Faster
How do you speed up the build of your Swift project? Do you use -warn-long-function-bodies
or maybe even -stats-output-dir
? These can find some compilation performance issues, but how do you truly take your build times to the next level?
In this article, I want to show you how tweaking some build settings can drastically speed up your clean build times.
Let us start by building Avito with the default Xcode build settings. For the benchmark, I am using M1 Pro
with 32 GB
RAM. Here is the result:
So our clean build time is somewhere around 240 seconds. Is that good? Is that bad? Let us compute some stats for our project.
A way to measure the size of the project is to use a cloc utility.
Here is what we got:
- 1 240 000 lines of Swift code
- 100 000 lines of Objective-C code (mostly external dependencies)
- 6000 lines of C code
I would say that 240 seconds is not bad. But can we go faster? Let us switch SWIFT_COMPILATION_MODE
from incremental
to wholemodule
and see where that leads.
This change gets us to the following build duration distribution:
Now the average build time is 179 seconds. Somehow our build got faster by 25%!
How well is your build parallelized?
To understand why the build became faster, we first need to know the difference between incremental
and wholemodule
compilation modes.
The summary is:
- When we build with
incremental
, the build system uses a batch mode for the swift compiler. The batch mode splits each module compilation into multiple jobs and executes those jobs in parallel. - On the other hand,
wholemodule
runs a mostly single-threaded compilation of the module without splitting it in any way.
So incremental
looks like it should be faster due to better parallelization, but why does wholemodule
perform better in the end?
incremental
build is slower because the compiler has to do more work compiling a module in batches rather than compiling a module as a whole which results in some overhead.- At the same time, even though the
wholemodule
compilation is single-threaded, many modules still build in parallel, making this mode efficient.
The compiler documentation also makes a note that wholemodule
could be faster than incremental
under circumstances where many modules build in parallel:
It is, therefore, possible that in certain cases (such as with limited available parallelism / many modules built in parallel), building in whole-module mode with optimization disabled can complete in less time than batched primary-file mode
So how well is build at Avito parallelized?
To understand that, we will employ a visualization similar to that introduced in Xcode 14. It allows us to see how many modules build in parallel at a particular time.
Let us first take a look at Avito built with incremental
compilation mode:
Here colored rectangles represent heavy Xcode build system invocations: swiftc
, ld
, and some other tools such as actool
. The build seems well parallelized with the incremental
compilation mode.
Now take a look at the visualization produced with wholemodule
compilation mode:
We can see where Avito build is faring well and where it could do better. The reason for this specific shape of the graph is our architecture. At first, we build different utilities which don’t have many dependencies, then follow the poorly parallelized parts of the build - monolithic modules. At last, we have feature modules that don’t depend on one another and build in parallel.
Another way to look at build performance is by looking at CPU utilization. The Instruments CPU Profiler
trace maps to the wholemodule
graph quite well:
As expected, there is a sag in CPU utilization where parallelization isn’t perfect.
Squeezing the last bits of build performance
In an ideal build scenario, all modules would build parallel across all available cores. Unfortunately, mistakes in architecture can make such a goal unattainable.
However, there is something we can do to make the build more efficient without reengineering everything from scratch.
What if we try to leverage the best of wholemodule
and incremental
simultaneously? We can keep using wholemodule
where the build process is parallelized well and switch to incremental
for those monolithic modules in the middle. That leads to the following build distribution:
This change gave us another 14 seconds! The CPU is loaded much more evenly, and gaps in the build graph are smaller.
What’s next?
Here we only looked at clean builds. Next time I will tell you all about the incremental builds at Avito!
Does tweaking compilation modes make your builds faster? How large is your project, and how swiftly does it build? Let me know in the comments!