Compilation speed: help test batch mode!

Just so you know, if you do this on every build you're wasting a good chunk of your build time on loading the SDK. You might want to measure the difference between doing this and not even with classic single-file or WMO!


Thanks everyone for all the bug reports!

I'm only doing it for my build script I use for doing this timings so that one build doesn't spend time populating this cache and bring down the first build's timing compare to other builds. Does it make sense to do that, or am I misunderstanding how this is used? IIRC, 3rd party modules get to use this cache as well.

Will you let us know about the snapshot w/ assertions disabled here?

1 Like

Seeing some improvements, but not on the magnitude I was hoping. Definitely had to comment out choice pieces of code for compiler assertions.

Here's what we got:
03-15 toolchain - debug - wholemodule: 198.543s
03-15 toolchain - debug - singlefile: 538.085s
03-15 toolchain - debug - batch: 469.921s

Rough metrics:
42kloc *swift
7kloc *.m

I'm still not able to completely build my projet in normal or batched mode :confused: .

I have filled a JIRA and radar with our company code.

$ cloc .

   13345 text files.
   10587 unique files.                                          
    4397 files ignored.

github.com/AlDanial/cloc v 1.72  T=22.59 s (127.9 files/s, 9293.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Objective C                    465          18192           6351          53471
Swift                          773          19049          12330          52960
JSON                          1115              0              0          24276
C/C++ Header                   527           5659           9755           6381
XML                              7              0             58           1342
Markdown                         3             41              0             68
-------------------------------------------------------------------------------
SUM:                          2890          42941          28494         138498
-------------------------------------------------------------------------------

Toolchain: Swift Development Snapshot 2018-03-17
core: 4 (i7 2.9Ghz - MacBookPro13,3)
wmo: 306.610s
normal: (assertion)
batch: (assertion)

Hi everyone!

Thanks to @mishal_shah there's now a "no assertions" snapshot available for testing batch mode.

The instructions above have been changed to reflect the link. If you tried batch mode with the previous assertion-enabled snapshots and had trouble (assertion-failure crashes or suboptimal timing results) I'd encourage you to give this one a try.

Thanks for all the feedback to far.

Compiling a project with ~55k lines of Swift code (360 files), split up into multiple packages. Timings also include C++/Metal packages and other Xcode build process steps, but it's predominantly Swift compilation.

Using the no-assert toolchain:

Debug build without batch mode: 81.6s
Debug build with batch mode: 62s

I couldn't build with -wmo in debug configuration ("index output filenames do not match input source files"), although release with WMO still works.

Build computer is a 2012 rMBP – 2.6GHz, 4 cores.

The batch-mode snapshot is causing a linker error in my project which I can't reproduce with the toolchain bundled with Xcode 9.3 beta4:

Undefined symbols for architecture arm64:
  "_$SSo12UIRectCornerVs10SetAlgebraSCWa", referenced from:
      _$SSo12UIRectCornerVABs10SetAlgebraSCWl in ErrorViewController.o

You will likely need to clean the build and delete the derived data directory. Does this failure occur in that case?

I'm hitting a compiler crash when trying to build something equivalent to (reduced to) the following:

let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)

let completion: ((Bool) -> Void) = { _ in
// nothing in the closure
}
collectionView.performBatchUpdates(nil, completion: completion)

With the following stack track:

0  swift                    0x0000000109b96cf8 llvm::sys::PrintStackTrace(llvm::raw_ostream&) + 40
1  swift                    0x0000000109b973a6 SignalHandler(int) + 598
2  libsystem_platform.dylib 0x00007fff682e5f5a _sigtramp + 26
3  libsystem_platform.dylib 0x00007fb3502eaa00 _sigtramp + 3892333248
4  swift                    0x0000000107355838 checkForViolationsInNoEscapeClosures(llvm::SmallDenseMap<(anonymous namespace)::AccessedStorage, (anonymous namespace)::AccessInfo, 4u, llvm::DenseMapInfo<(anonymous namespace)::AccessedStorage>, llvm::detail::DenseMapPair<(anonymous namespace)::AccessedStorage, (anonymous namespace)::AccessInfo> > const&, swift::FullApplySite, swift::AccessSummaryAnalysis*, llvm::SmallVectorImpl<(anonymous namespace)::ConflictingAccess>&) + 328
5  swift                    0x000000010735436f (anonymous namespace)::DiagnoseStaticExclusivity::run() + 4287
6  swift                    0x00000001072eb689 swift::SILPassManager::runPassOnFunction(unsigned int, swift::SILFunction*) + 1129
7  swift                    0x00000001072ec375 swift::SILPassManager::runFunctionPasses(unsigned int, unsigned int) + 1205
8  swift                    0x00000001072eccfc swift::SILPassManager::execute() + 348
9  swift                    0x00000001072f278c swift::runSILDiagnosticPasses(swift::SILModule&) + 332
10 swift                    0x0000000106ada6cd performCompile(swift::CompilerInstance&, swift::CompilerInvocation&, llvm::ArrayRef<char const*>, int&, swift::FrontendObserver*, swift::UnifiedStatsReporter*) + 12093
11 swift                    0x0000000106ad6802 swift::performFrontend(llvm::ArrayRef<char const*>, char const*, void*, swift::FrontendObserver*) + 3266
12 swift                    0x0000000106a94157 main + 2183
13 libdyld.dylib            0x00007fff68064115 start + 1
14 libdyld.dylib            0x00000000000001cd start + 2549727417

If I comment out the performBatchUpdates line then I get linker errors similar to @hartbit

Undefined symbols for architecture x86_64:
  "_OBJC_CLASS_$__TtC7Sample27ProfileMainViewController", referenced from:
      objc-class-ref in SAAMoreMenuViewController.o
  "_OBJC_CLASS_$__TtC7Sample25FSDetailsViewController", referenced from:
      objc-class-ref in SAAResultsViewController.o
  "_OBJC_CLASS_$__TtC7Sample18FirewallAuthToken", referenced from:
      objc-class-ref in SAADataStore.o
  "_OBJC_CLASS_$__TtC7Sample31DetailListUnifiedViewController", referenced from:
      objc-class-ref in SAAAppDelegate-29FF8FF3F5D95CA3.o
  "_OBJC_CLASS_$__TtC7Sample31NotificationParameters", referenced from:
      objc-class-ref in SAAItem.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

This is in a mixed ObjC & Swift project with ~100kloc of each Swift and ObjC and builds properly with Version 9.3 beta 4 (9Q127n) toolchain.

@daveanderson, can you file a bug for the compiler crash you hit?

I don't know about the linker error, but the exclusivity checking crash will be fixed by SILOptimizer: Fix exclusivity checking crash when passing 'nil' as optional noescape closure by slavapestov · Pull Request #15390 · apple/swift · GitHub.

1 Like

I already nuked the derived data folder. This linker error seems legit.

If you can, please reduce test cases and file bugs for those assertions.

I'll add that this is generally bad advice, because we do want users to test snapshots with asserts enabled -- asserts indicate real bugs that we need to fix, and a no-assert build might silently miscompile the code instead.

However, for performance testing, no-asserts is the way to go, since they're generally much faster.

1 Like

Of course! There appeared to be no rhyme or reason to them at the time, but I'll be sure to try and reduce again over the next week.

I had the same SILFunctionTransform segfault ([SR-6862] SILFunctionTransform crashes · Issue #49411 · apple/swift · GitHub) so I commented out those lines. After that, I had a bunch of linker errors (native Objective-C code can't find symbols for Objective-C classes defined in Swift) but ignoring those this is what I got:

Files & lines of code in main build target:

Swift: 300,973 LOC across 2,275 files
Obj-C: 411,515 LOC across 1,654 files

-Onone, Single File: 1357.8s
-Onone, Single File, -enable-batch-mode: 316.2s
-Onone, Whole Module: 320.9s

-Onone, Single File, -enable-batch-mode, incremental build after changing a comment in a Swift file: 54.6s (I guess you could use as a baseline for the other build times. Swift was building for ~13s and merging the swift module for ~23s of that.)

System specs:
macOS High Sierra 10.3.3
iMac Retina 5K 27", Late 2015
4 GHz Intel Core i7
32 GB 1600 MHz DDR3
2GB Fusion Drive (so HFS+, not APFS)

3 Likes

Is there a possibility you might be willing to share your project with us? 13s to rebuild a single file seems extreme. Some low hanging fruit might be ripe for picking in merge-modules also...

I don't know how helpful those metrics are since this is measured on an iOS project with external dependencies (managed by cocoapods). I think the whole build process also includes things like compiling the Xibs and Objective-C sources so not all the time is spent on actually compiling Swift code.

cloc output for the whole project (with external pods):

Compilcation speed:
Debug -Onone compiles in 101 - 107 seconds
Debug -Onone with batch mode compiles in 83 - 88 seconds

The build time is different each time so I run it twice for both settings to get the compilation time range.
Hope this helps.

Overall I am seeing significant compilation speed improvements over the current "official" toolchain that ships with Xcode 9.2. On that toolchain, the average compilation speed of the same project in Debug -Onone settings is around 150 seconds. That means a 45% decrease in compilation time for this particular project. Well done!

We have a large project, which takes a long time to compile, so we are really looking forward to this change (or the idea of it at least, see the Summary section at the end). We currently use WMO for our Debug builds since changing branches seems to hit the "rebuild everything" case too often in Single File mode, and thus we are optimizing around that.

Structure

Here is our project structure and stats:

Target Files LOC
Module A 116 8,404
Module B 717 43,593
Module C 77 5,088
App 741 52,338

Module C depends on A and B, App depends on All.

Times

I tested both Clean builds and Incremental with all 3 compilation modes. For incremental I added/removed one line of code from within a method. I also noted "Failed Incremental" times, which I'll explain after.

Single File

Clean: 265

Target Incremental Failed Incremental
Module A 8 129
Module B 15 137
Module C 14 128
App 17 107

WMO

Clean: 115

Target Incremental Failed Incremental
Module A 14 47
Module B 31 70
Module C 14 42
App 27 41

Batch

Clean: 145

Target Incremental Failed Incremental
Module A 8 73
Module B 15 83
Module C 9 79
App 13 74

Incremental

In order to get the "Failed Incremental" times I simply added or removed an internal property to a class. This consistently caused longer compile times in all compilation modes, because they caused the App to do a non-incremental build. I feel this shouldn't, since it doesn't change the exposed API in any way (even though it change the structure), but :man_shrugging:.

Something that consistently caused a Failed Incremental in Batch, but not the other compilation modes, was editing the body of a lazy property. This confused me at first and made me feel that Incremental compilation wasn't working correctly since it was my go to thing to change before determining that I should instead change the body of a method. This is a regression IMO.

Summary

  • Clean build is slower than WMO but much better than Single File. It would be great if this was at least the same as WMO.
  • Incremental compilation of Batch is basically Single File level or better. :tada:
  • The Failed Incremental for Batch seems to be on the high side. It's nearly double that of WMO. And with the regression on how often this can happen, it makes me concerned.

As it stands I'm not 100% sure we would switch to this over WMO for our Debug builds. If the Failed Incremental was improved to be at least the same as WMO, we would use it for sure.