Demystifying Explicitly built modules for Xcode

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 (by spawning a new clang or swiftc process) into a binary file (*.swiftmodule for Swift and *.pcm for Objective-C). For example, when compiling MySwiftUIView.swift that imports the SwiftUI module, the SwiftUI 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 as part of one Xcode build on a multi-core CPU, the smaller tasks of a compilation were not coordinated efficiently across the CPU cores because xcodebuild was only aware of the compilation of a source file, not the sub-tasks performed by the compiler. 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 built 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 phases:

  • Scanning: xcodebuild scans all source files of the project and builds a graph of all imported modules
  • Compile imported modules: xcodebuild compiles the imported modules identified during the scanning phase, ensuring that all reference modules are ready for the final compile step.
  • Compile 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 scan phase, 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 is 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 we 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.

Get Started for free

Start building now, choose a plan later.

Sign Up

Get started for free

Start building now, choose a plan later.