Swift 6 / Xcode 16.1 slower build times

Another year, another swift release, another huge regression in build times. Traditions, it seems.

Incremental builds are super slow with Xcode 16.1: we went from 20s to 90s on an M1, just by updating Xcode.

We disabled the Module Verifier already, but build times are still really bad. Even changing a single character seems to rebuild more than 50% of the project. We didn't see this in Xcode 15.

Are there any other know flags/bugs in Swift 6 that will help us to get back to normal build times?

1 Like

Please gather some concrete timing data for different settings and report it here and to Apple. It's the only way to get this stuff fixed.

I have noticed that there are a few main causes of build regressions in Xcode 16, though it's impossible to tell if they're Swift or Xcode regressions.

  1. Bizarrely, the thread sanitizer is suddenly taking a huge amount of overhead during builds, both clean and incremental. Turning it off saved us a huge amount of time.
  2. The new explicit module build setting, which is disabled by default, also regresses, so make sure you haven't turned that on.
  3. The emit module step seems to be especially slow now, even for incremental builds. With our ~1300 file project, even incremental builds have a 25 - 30s emit module step. I'm not sure why this is so slow or unable to be properly incremental, but there doesn't seem to be a way to fix it.
  4. Speaking of the emit module step, it seems to wait for macro expansion to complete, so in clean builds it takes a whole processor core to itself for about 90s.
  5. Speaking of macros, they're still very slow, but I don't know if there's any particular regression here. The expansion does seem to force everything else that depends on the true source to wait.

Those are our big remaining issues now that we moved 1300 images out of our asset catalog.

4 Likes

We see the same. Going from Xcode 15.4 -> 16.1 builds and installing to the simulator is hauntingly slow. Even previews are terrible slow at compiling.

I have done iOS apps since the first iPhone came out and the developer experience Apple leave you with has always been half baked, compared to other platforms and languages.

Some non-scientific results, but it's quite shocking to see the slow down over time GitHub - devMEremenko/XcodeBenchmark: XcodeBenchmark measures the compilation time of a large codebase on iMac, MacBook, and Mac Pro

1 Like

We have been seeing this as well.

Looking at our build graph, anything that includes our -Swift.h generated header and is Objective-C++ have taken a huge hit. Upwards of 2 minutes to compile those files. When we remove the -Swift.h include (and refactor that code out of those files or covert it to Objective-C or C++) the compile times drop to a few seconds.

Using the -ftime-trace flag plus ClangBuildAnalyzer (GitHub - aras-p/ClangBuildAnalyzer: Clang build analysis tool using -ftime-trace) where were able to determine that module loading of the -Swift.h generated file is the majority of the excess build time in Xcode 16 vs 15

We've been working on a bringing this same effect to a smaller demo project than our 2mil+ LOC main project so we can share something with Apple, but we are not quite there yet. Hopefully soon.

This is a good resource to get started with analyzing clang issues.
https://aras-p.info/blog/2019/01/16/time-trace-timeline-flame-chart-profiler-for-Clang/

I don't have any guidance if your project is all Swift though.

It would be interesting to see a profile, can copy and paste the swift-frontend invocation and record one?

xctrace record -t 'Time Profiler' --target-stdout - --launch -- swift-frontend -emit-module ...
1 Like

Finding the one particular build command that triggers the behavior I see is essentially impossible, so I've captured a full system trace that shows a swift-frontend process consuming CPU for the exact time I see in Xcode's build log for Emit Modules. However, most of the work in that process seems to be type checking, but I'll let you figure it out. It's FB15727440, once the stuff finishes uploading.

Edit: Upload now complete.

3 Likes

So -emit-module type checks all declarations in all source files in one shot, but it skips non-inlinable function bodies. It looks like in your example, 2/3rds of the -emit-module time is actually spent in macro expansion. Unfortunately I’m not familiar enough with macros to offer any concrete suggestions here.

Good to know, thanks. Hopefully we can get some performance improvements for macros at some point.