Should `-j 1` be used for swiftc invocations?

It looks like various build systems (including xcodebuild?) treat swiftc invocations like clang invocations, where each process is primarily single-threaded. They run one concurrent swiftc process per logical core. However, swiftc can use multiple threads, so it seems that there'll be a contention issue. Not to mention, potential increased overhead from synchronizing the threads and false sharing of cache lines.

With that in mind, should swiftc always be run with -j 1 to ensure that it's single-threaded?

I'll answer since I've looked into this a bit, at least on linux. I believe the default is to just use a single process with a single thread, so you should be fine just leaving that flag off. If you want to use multiple cores, you can add -j5 to spawn a maximum of five frontend processes. There's also the -num-threads 5 option, if you'd rather spawn five threads.

When using the Swift Package Manager (SPM), the default when building in debug configuration is to tell the swiftc process to spawn as many processes as system cores, which you can modify by passing -j 5 (note the space) to swift build, let's say. However, SPM will try to spawn a swiftc for each target, so that can lead to the kind of thrashing you're worried about, particularly if you don't limit it with -j 5 or less. When building in release configuration, each swiftc is passed -num-threads with the total number of cores and you cannot mitigate it.

Right now, Swift only uses multiple threads in -wmo mode, which only has one frontend job anyway, so you shouldn't get CPU oversaturation with just a single swiftc invocation. (Either you get up to N frontend jobs with 1 thread, or 1 frontend job with up to N threads.)

The question still makes sense at a higher level: should build systems try to build multiple targets at once, or do non-swiftc tasks at the same time as swiftc invocations? We* actually looked into this for Xcode a few years ago and found that even with the increased contention, project build times nearly always still improved, because the fanout for most projects isn't big enough to cause a problem in practice, especially with process startup times and the fact that usually there are some files in a target that compile quickly and some that lag behind. Still, though, this is an approximation, and the work on the new swiftc driver to allow integration directly into the package manager's scheduler should allow for a more careful CPU saturation than what happens today.

* I used to work at Apple.

3 Likes

FWIW, SwiftPM gained experimental support for the integrated Swift driver just last week. If you grab SwiftPM from master and pass the --use-integrated-swift-driver option to SwiftPM, it will use the integrated driver. What we hope for is that you'll get the same results, but SwiftPM will be able to better manage the concurrency because each of the swift processes will be single-threaded (for debug builds).

Note that this won't have much of an effect for release (whole-module-optimization) builds, because those still use a single "frontend" job.

I'd love for folks to try out --use-integrated-swift-driver and tell us how things go.

Doug

4 Likes

I thought that with a set of single-threaded jobs (assuming disk contention is not a bottleneck) you could just keep one job going per logical core to get optimal results. Is there more nuance than that?