The struggle for the build time of iOS applications
A little over a month ago, we released the Tinkoff Investments iOS application. The application is fully written in Swift, but has some Objective-C dependencies. The product quickly began to acquire new functionality, and at the same time, the assembly time of the project was significantly increased. When we came to the conclusion that after clean or significant edits the project took longer than six minutes, we realized that change was necessary.
On the Internet, many effective and not very ways were found to speed up the assembly time of the project. We were especially interested in the build time of the debug version, because it became increasingly difficult to work. Below I will talk about the methods that we tested as part of solving the problem, and the results that we have achieved. I want to note that a long build time may depend on various factors, therefore, the methods for each project are different.
Since Swift is still young, some of the sweet syntax constructs can cause compiler misunderstanding. To evaluate the most difficult functions to compile, you can use the Swift analyzer built into the compiler. The easiest way to get a report is to build the project from the console with this command:
where “App.xcworkspace” is the name of the workspace file of your project, “App” is the name of the scheme by which you want to build.
We pass the flags "-Xfrontend -debug-time-function-bodies" to debug the compilation process and track the compilation time of each function. Using grep, we select the lines containing the compilation time, then output the sorted result to the functions_build_analysis.txt file.
Using this report, we found several functions that were difficult to compile, one of which was going to take 17 seconds, and the other was 6. The main reason for such deplorable results is the use of “Nil Coalescing” in the object constructor. In the code there were such constructions:
We took the calculation of the parameter value to a separate line above, and the problem was solved - the compilation time of functions was reduced to 300 milliseconds.
This is not the only surprise that the Swift compiler can present to you. The main problems of the long assembly of individual functions are related to the definition of variable types. Most often this is due to the use of the "??", "?:" Operators in the constructors of objects, dictionaries, arrays, as well as in the concatenation of strings and arrays. You can read an article with interesting observations about speeding up build time through code refactoring.
The total gain that we were able to get from manipulating the code was 30 seconds, which was already a good achievement.
In the debug assembly, the project is compiled only for the architecture of the device selected for debugging. That is, choosing Yes here, we theoretically speed up the assembly time by half.
Since in our project this flag was already set to Yes, we did not win at this point. But for the sake of experiment, we tried the assembly with the flag set in No. To do this, I had to tinker with Pods, because they were also ready to provide compiled code only for active architecture. The build time of the project in the end was 10 minutes 21 seconds, which is actually almost twice as much as the original.
The Swift compiler has a flag called " -whole-module-optimization ". He is responsible for how the files will be processed during compilation: whether they will be compiled one at a time or immediately assembled into a module. In Xcode, you can control this flag using the “Optimization Level” section, but by default only these options are available to us:
Using Whole Module Optimization significantly reduces the compilation time of debug assemblies. But along with this flag, we add the “-O” flag, which turns on the SIL optimizer, and there is a problem - the project ceases to be debugable. In the console, we see the following:
To preserve the ability to build the whole module at once and turn off optimization, you can add the “-Onone” flag in the “Other Swift Flags” section. As a result, for debugging, we get an assembly that is assembled as quickly as possible by disabling all kinds of optimizations. In our project, this yielded amazing results - the debug build speed increased almost 3 times.
There is another compiler flag that helps reduce compilation time. But it works only for assemblies without the “-whole-module-optimization" flag and, more likely, can be useful for Release assemblies. This is the flag “ -enable-bridging-pch ”.
It does not help in all cases, but only in projects with Bridging headers from Objective-C. The effect is that every time during the build, the compiler does not rebuild the bridging table of the Objective-C methods in Swift.
For our project with the “-whole-module-optimization” flag turned off and “-enable-bridging-pch” turned on, the time gain was about 15% .
According to the results of the study, there are two main ways to speed up compilation of your Debug assembly: optimization of the code itself for the compiler and the use of the “whole-module-optimization” flag. We managed to reduce the clean build time of the project from 6 minutes to 1 minute 20 seconds, half of which is the assembly of third-party dependencies. If you have any experience with the Swift compiler, share in the comments.
PS: test hardware:
Mac mini (Late 2012)
2.3 GHz Intel Core i7
16 GB 1600 MHz DDR3
250 GB SSD
On the Internet, many effective and not very ways were found to speed up the assembly time of the project. We were especially interested in the build time of the debug version, because it became increasingly difficult to work. Below I will talk about the methods that we tested as part of solving the problem, and the results that we have achieved. I want to note that a long build time may depend on various factors, therefore, the methods for each project are different.
1. Pieces of code that are difficult to compile.
Since Swift is still young, some of the sweet syntax constructs can cause compiler misunderstanding. To evaluate the most difficult functions to compile, you can use the Swift analyzer built into the compiler. The easiest way to get a report is to build the project from the console with this command:
xcodebuild -workspace App.xcworkspace -scheme App clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep .[0-9]ms | grep -v ^0.[0-9]ms | sort -nr > functions_build_analysis.txt
where “App.xcworkspace” is the name of the workspace file of your project, “App” is the name of the scheme by which you want to build.
We pass the flags "-Xfrontend -debug-time-function-bodies" to debug the compilation process and track the compilation time of each function. Using grep, we select the lines containing the compilation time, then output the sorted result to the functions_build_analysis.txt file.
Using this report, we found several functions that were difficult to compile, one of which was going to take 17 seconds, and the other was 6. The main reason for such deplorable results is the use of “Nil Coalescing” in the object constructor. In the code there were such constructions:
let object = Object(param1: param1Value ?? defaultParam1Value,
param2: param2Value ?? defaultParam2Value)
We took the calculation of the parameter value to a separate line above, and the problem was solved - the compilation time of functions was reduced to 300 milliseconds.
This is not the only surprise that the Swift compiler can present to you. The main problems of the long assembly of individual functions are related to the definition of variable types. Most often this is due to the use of the "??", "?:" Operators in the constructors of objects, dictionaries, arrays, as well as in the concatenation of strings and arrays. You can read an article with interesting observations about speeding up build time through code refactoring.
The total gain that we were able to get from manipulating the code was 30 seconds, which was already a good achievement.
2. Build only the selected architecture for Debug builds.
In the debug assembly, the project is compiled only for the architecture of the device selected for debugging. That is, choosing Yes here, we theoretically speed up the assembly time by half.
Since in our project this flag was already set to Yes, we did not win at this point. But for the sake of experiment, we tried the assembly with the flag set in No. To do this, I had to tinker with Pods, because they were also ready to provide compiled code only for active architecture. The build time of the project in the end was 10 minutes 21 seconds, which is actually almost twice as much as the original.
3. Whole Module Optimization.
The Swift compiler has a flag called " -whole-module-optimization ". He is responsible for how the files will be processed during compilation: whether they will be compiled one at a time or immediately assembled into a module. In Xcode, you can control this flag using the “Optimization Level” section, but by default only these options are available to us:
Using Whole Module Optimization significantly reduces the compilation time of debug assemblies. But along with this flag, we add the “-O” flag, which turns on the SIL optimizer, and there is a problem - the project ceases to be debugable. In the console, we see the following:
App was compiled with optimization - stepping may behave oddly; variables may not be available.
To preserve the ability to build the whole module at once and turn off optimization, you can add the “-Onone” flag in the “Other Swift Flags” section. As a result, for debugging, we get an assembly that is assembled as quickly as possible by disabling all kinds of optimizations. In our project, this yielded amazing results - the debug build speed increased almost 3 times.
4. Precompiled Bridging Headers.
There is another compiler flag that helps reduce compilation time. But it works only for assemblies without the “-whole-module-optimization" flag and, more likely, can be useful for Release assemblies. This is the flag “ -enable-bridging-pch ”.
It does not help in all cases, but only in projects with Bridging headers from Objective-C. The effect is that every time during the build, the compiler does not rebuild the bridging table of the Objective-C methods in Swift.
For our project with the “-whole-module-optimization” flag turned off and “-enable-bridging-pch” turned on, the time gain was about 15% .
Summary
According to the results of the study, there are two main ways to speed up compilation of your Debug assembly: optimization of the code itself for the compiler and the use of the “whole-module-optimization” flag. We managed to reduce the clean build time of the project from 6 minutes to 1 minute 20 seconds, half of which is the assembly of third-party dependencies. If you have any experience with the Swift compiler, share in the comments.
PS: test hardware:
Mac mini (Late 2012)
2.3 GHz Intel Core i7
16 GB 1600 MHz DDR3
250 GB SSD