Note
A version of this post also appears on the Bitrise blog.
One of the new features of Xcode 16 is called “explicitly built modules”. Behind this abstract name is something that makes builds faster and compiler errors more informative. As this is enabled by default for C and Objective-C code, you can experience some of the benefits instantly, but it can also be enabled for Swift code as an experimental feature.
In this post, we’ll explore how this feature works and the benefits it brings to projects that adopt it. We’ll also look at the build graphs of actual projects and run some benchmarks.
xcodebuild in a nutshell
Before we begin, let’s define what we mean by “compiler” and the “build system”. When using Xcode, builds are delegated to the xcodebuild
CLI tool. This is not a compiler itself, but more like a build system. It understands your Xcode project structure, invokes actual compilers, performs additional build tasks like codesigning, and orchestrates all the tasks required to build a target. xcodebuild
is also the tool that powers builds in CI systems. Next, there are two compiler toolchains that are language-specific, clang
for C/C++/Objective-C, and swiftc
for Swift. xcodebuild
delegates tasks to clang
or swiftc
, depending on the language of source files. This layering of the different tools is an important detail and we’ll come back to it soon!
What are modules?
Modules, from the compiler’s perspective, are the units of code organization and distribution. It’s the module that you import when writing import UIKit
in a source file.
With Swift code, the various .swift
files in a target represent a single module. The public interface of the module is shaped by the access control keywords of the language. Objective-C code is organized into modules in a more manual way, and I’ll skip this for simplicity, but you can check out the first part of this WWDC video to get the full picture.
Next, let’s look at what happens during compilation. When your project is compiled, each imported module is compiled in isolation into a binary file (*.swiftmodule
for Swift and *.pcm
for Objective-C). For example, when compiling MySwiftUIView.swift
that imports the SwiftUI
module, that module needs to be compiled first in order to compile MySwiftUIView.swift
.
Traditionally, xcodebuild
delegated all of this to the actual compiler (swiftc
in this case) and modules were built “implicitly”. When building multiple source files and even targets as part of one Xcode build on a multi-core CPU, the actual compilers coordinated among themselves building the modules, while xcodebuild
was only aware of the compilation of a source file. This resulted in some long-running build tasks (from the view of xcodebuild
) where some tasks were waiting for a module to be compiled by another task. For example, if we have two Swift source files that both import SwiftUI
, then the compilation timeline looks like this with implicitly build modules:
The blue boxes represent xcodebuild
’s view of the compilation process. Each blue box takes up one “execution lane” (one CPU core), even when it’s not doing actual work, just waiting for a module to be compiled!
Explicitly built modules
The main idea behind Xcode 16’s new feature is to make the build graph more granular and let xcodebuild
understand the smaller tasks so that it can orchestrate work more efficiently. In this mode, compilation is split into three separate phases:
- Scanning:
xcodebuild
scans all source files of the project and builds a graph of all the imported modules - Building imported modules
- Building the source files: after a source file’s module dependencies are compiled, the source file itself can be compiled.
These are the three phases of compilation, but in practice, multiple source files need to be compiled, so the three phases are interleaved and the tasks are dispatched continuously as Xcode processes the source files. A real-world timeline looks more like this:
Let’s contrast it with a build of the same target, but with implicitly built modules:
Notice the number of tasks and the length of tasks in each timeline. The explicitly built module graph is much more granular, while the implicitly built graph has fewer and longer build tasks. These longer build tasks, as we now know, do more than just compiling a single source file. Compiling the module dependencies is also part of these tasks, just not visible to Xcode.
In explicit mode, we can also see that the SwiftUI module compilation is on the critical path and there is a period where other work is blocked by this compilation. Also, the explicit mode reveals that SwiftUI is compiled three times in three execution lanes, we’ll come back to it and what this means.
Additionally, the new compiler phases are top-level build tasks, so they are shared between targets. With a sufficiently complex project hierarchy, this deduplicates some work across targets and saves additional time.
Benchmarks
This all sounds great, does this make my builds faster automatically? Well, the answer is not a simple yes, or at least not for now.
The following benchmarks were run with Xcode 16 Beta 1, and as of this release, explicit module building is actually slightly slower than the old system. It’s not clear if this is caused by bugs in the initial Xcode beta (that will get fixed before the final release) or if the benefits depend on the structure and size of the project. Maybe there is a break-even point in complexity and target count after which explicitly built modules are faster?
- Tuist: not a project using Tuist, but the Tuist codebase itself)
- Xcode 16 default (C and Objective-C yes, Swift no): 50.1s
- Explicit everything: 57.8s
- BackyardBirds: a multi-target sample app from WWDC 2023.
- Xcode 16 default (C and Objective-C yes, Swift no): 21s
- Explicit everything: 30s
- pocketcasts-ios: a reasonably large real-world project
- Xcode 16 default (C and Objective-C yes, Swift no): 119s
- Explicit everything: 121s
Notes: tested on Apple Silicon M1 Pro (10 core) with 32GB RAM. Numbers mean clean builds.
Other benefits
Compilation speed and more efficient scheduling are not the only benefits of explicitly built modules:
- More deterministic builds thanks to the explicitness of build tasks
- More relevant error messages when a module compilation fails
- Faster debugging in Xcode: the debugger can now reuse the module graph constructed during the build, instead of discovering the modules again when launching the app.
Make sure to check out the WWDC video if you want to learn more about these in detail.
Enable explicitly built modules
Xcode 16 enables explicitly built modules by default for C and Objective-C code. For Swift code, it can be enabled as a project-level build setting in Xcode, just use the search box in the All
view:
Behind the scenes, this adds _EXPERIMENTAL_SWIFT_EXPLICIT_MODULES = YES
to the pbxproj
file.
Multiple module variants
When enabling this new build mechanism, you’ll notice that some modules are built multiple times, such as SwiftUI
in the above timeline view screenshot. Now that xcodebuild
is aware of what it takes to build your source files, it can surface this information. A module is compiled multiple times because some build settings across targets require the module to be built slightly differently. This was previously not visible in Xcode, and not necessarily a problem, but sometimes the number of variants can be reduced by unifying build settings across targets. For example, unifying preprocessor macros and moving them to the project level could eliminate module variants. Investigating variants and possible root causes is covered in the second part of this WWDC video
Wrap-up
So where does this leave us? The performance impact of explicitly built modules in the initial Xcode 16 release may be mixed, but I look forward to new Xcode betas and real-world benchmarks on more projects. By giving developers deeper insights into the module graph and build task dependencies, it also makes builds more deterministic and error messages more meaningful.