About Me

Effect of Swift Extension and Libraries On Build Time

I'm a huge fan of extensions in Swift. I use them liberally to separate logical parts of my types, conform to different protocols, and implement inner workings of the type (in private extensions). However, a coworker recently brought up a great point that it might have a negative effect on build times.

I thought the effect in a small and moderately size project must certainly be negligible, but what about large projects? Was my tendency to use many extensions causing daily pain in build times? I wanted to explore this question further so I setup some tests and did some benchmarking.

Question

Does separating code into extra extensions and/or modules have a meaningful impact on compile times?

Background

Before I jumped straight to doing my own tests, I did a search for existing articles online. I found a Github repository from the Swift 1.2 days written by Dmitry Bespalov: Swift Extension Performance.

In his testing of Swift 1.2, he found that the compilation time went up significantly faster when using extensions when the method count started exceeding 5000.

However, when I updated the script for 5.2 and ran the benchmarks again, I got almost exactly the opposite result:

However, this script only creates a single file so it isn't really representative of a real-world project. My script, attempt to get much closer to a real world project.

Method

You can see the full code of the script I wrote to perform these benchmarks on Github: drewag/effect-of-swift-extension-and-libraries-on-build-time.

I designed the script to emulate an average project as it scales up while varying the number of libraries as well as if extensions are used or not. I also wanted to see if there was a meaningful effect on rebuild times after making a simple method addition.

Project Generation

The first important step for the script is that it generates the different types of projects to be built. Each class is generated with the same structure:

  • 20 methods (separated into 4 different extensions if using that strategy)
  • A main method that calls every other method

If it is not using extensions, it is generated like this:

public class Class1 {
    public init() {}
    public func main() {
        method1()
        method2()
        method3()
        // continues like this for 20 methods
    }

    func method1() {
        for i in 0 ..< 1 { print(i + 2) }; print("finished")
    }

    func method2() {
        for i in 0 ..< 2 { print(i + 2) }; print("finished")
    }

    func method3() {
        for i in 0 ..< 3 { print(i + 2) }; print("finished")
    }

    // continues like this for 20 methods
}

If it is using extensions it looks like this:

public class Class1 {
    public init() {}
    public func main() {
        method1()
        method2()
        method3()
        // continues like this for 20 methods
    }
}
extension Class1 {

    func method1() {
        for i in 0 ..< 1 { print(i + 2) }; print("finished")
    }

    func method2() {
        for i in 0 ..< 2 { print(i + 2) }; print("finished")
    }

    // continues like this for 5 methods
}
extension Class1 {

    func method6() {
        for i in 0 ..< 6 { print(i + 2) }; print("finished")
    }

    func method7() {
        for i in 0 ..< 7 { print(i + 2) }; print("finished")
    }

    // continues like this for 5 methods
}
// continues like for 4 extensions

Along with generating n class files like this, it also generates a main.swift that calls into the main of every class:

Class1().main()
Class2().main()
Class3().main()

The goal of the main file and main methods is to ensure there isn't "unused code" that the compiler might be able to optimize away because in real project all of the code should be used from somewhere (ideally).

The last generation step is to organize the files into libraries. For this, the script generates a Package.swift and generates the files into the appropriate directory structure.

If using zero libraries, it is all generated as:

  • Package.swift
  • Sources
    • Test
      • main.swift
      • Class1.swift
      • Class2.swift
      • Class3.swift
      • etc

If instead libraries are being used, it distributes the classes evenly into each library:

  • Package.swift
  • Sources
    • Test
      • main.swift
    • Lib1
      • Class1.swift
      • Class2.swift
      • Class3.swift
      • etc.
    • Lib2
      • Class4.swift
      • Class5.swift
      • Class6.swift
      • etc.
    • etc.

Once the directory structure is fully generated, the script runs swift package generate-xcodeproj to generate an Xcode project to build.

Timing the Build

The script first runs a clean to ensure no existing artifacts affect the timing. Then, it runs the build with the following command:

xcodebuild -project Test.xcodeproj -scheme Test

It records how long it takes by taking note of the time before the build starts and compares that to the time at the end of the build. Note, for ease of scripting, it does not use the build time as reported by xcodebuild, but instead just the time the command runs for.

It then makes a change to Class1 by rewriting the file with 1 extra method and runs the build again to get the rebuild time for that configuration.

Collecting the Results

The script was run with possible library counts of 0, 1, 2, 4, and 8 as well as type counts of 128, 256, 1024, 2048, and 4096.

It times a clean build and then a build after adding a method to Class1 of every combination of those values. It also times each configuration 3 times and takes the average of them as the final reported build time.

Since builds can take a long time, the script goes through every combination before repeating a build of the same configuration. This is an attempt to reduce the chance that the available computer resources may change between builds.

The Official Run

For the official results, I ran the script on my 2014 iMac which has 4 cores and 32 GB of RAM. I also ran it with all non-necessary apps closed and with the internet and bluetooth turned off to reduce the chance of unrelated background activity spinning up during the test.

The Results

First, let's start with the raw results:

0 Libraries
Clean Build After Change Build
Type Count No Extensions (s) With Extensions (s) No Extensions (s) With Extensions (s)
128 3.73 3.76 2.24 2.25
256 6.27 6.19 2.69 2.69
1024 25.21 25.02 8.63 8.59
2048 67.52 69.51 25.39 25.29
4096 226.50 261.03 86.74 90.29
1 Library
Clean Build After Change Build
Type Count No Extensions (s) With Extensions (s) No Extensions (s) With Extensions (s)
128 3.95 3.91 2.30 2.26
256 6.31 6.28 2.72 2.76
1024 24.66 24.78 8.72 8.60
2048 66.53 69.47 24.83 25.06
4096 223.14 257.18 87.57 88.20
2 Libraries
Clean Build After Change Build
Type Count No Extensions (s) With Extensions (s) No Extensions (s) With Extensions (s)
128 3.98 3.95 2.17 2.11
256 5.91 5.83 2.30 2.27
1024 19.34 19.38 4.37 4.34
2048 44.87 45.61 9.59 9.55
4096 130.31 134.07 28.23 28.32
4 Libraries
Clean Build After Change Build
Type Count No Extensions (s) With Extensions (s) No Extensions (s) With Extensions (s)
128 4.30 4.19 2.09 2.06
256 6.16 6.27 2.20 2.21
1024 17.68 17.80 3.25 3.15
2048 38.05 37.82 5.31 5.26
4096 95.92 96.08 12.18 12.05
8 Libraries
Clean Build After Change Build
Type Count No Extensions (s) With Extensions (s) No Extensions (s) With Extensions (s)
128 5.07 5.05 2.16 2.13
256 6.74 6.79 2.22 2.23
1024 20.47 20.41 2.89 2.86
2048 35.18 34.64 4.11 4.06
4096 81.22 80.58 7.76 7.59

Analysis

I've split the results into two different sections: clean builds and builds after a change.

Clean Builds

First, let's look at the clean build times:

The graph shows that the biggest effect on build times is the number of libraries used, especially once the project grows to 4096 types.

Also, the graph shows that there is at least a small gap in build times for 0 and 1 libraries, but virtually none for the rest. To get a better look at the increases in build times I graphed them separately:

It is now even more clear that the increase hovers around the zero mark regardless of the number of types for 2, 4, and 8 libraries. Having 0 or 1 libraries doesn't reveal a significant increase when using extensions until the project approaches 4096 types. At that point, it maxes out at around a 15% increase in build time when using extensions.

Builds After a Change

Now let's look at the results for the builds after making a change (without a clean). Here are the build times:

Here we see the same trend in build times as the project grows depending on the number of libraries, but the gap between with extension and without extension builds are basically non-existent. To be sure, let's zoom in on the percent change graph:

Here we can see that the biggest change we had was at 4096 types with 0 libraries. Even that only reached a 4% increase.

The last thing to focus on is the effect of the number of libraries on build times. These are the numbers for the extension strategy, but the without extension strategy is similar:

As you can see, there is enormous savings to both clean build and after a change build times when splitting up the classes into multiple libraries.

Discussion

Even though I made a strong attempt at representing real-life projects, there are still some shortcomings of this method.

  • The libraries do not make use of each other in any way.
  • The classes are distributed exactly evenly among all libraries which is not realistic.
  • The methods are distributed exactly evenly amount all extensions which is also not realistic.
  • This makes no use of properties on the classes.
  • There is no inheritance and no protocols.

There are probably other things I haven't thought of, but I still think this was an accurate enough approximation that the results are meaningful.

Conclusions

Clearly you can draw your own conclusions from the data, but this is what I've concluded.

  • Extensions have an insignificant effect, even when dealing with large projects, as long as those projects can be organized into several individual libraries. However, it still seems like a good idea to consider a simple comment separating two groupings of methods instead of an extra extension.
  • There are huge build time benefits to organizing your code into multiple libraries instead of one single behemoth.