Speed Up Swift: SwiftSyntaxMacros Package Compilation — Part 1

Ruslan Alikhamov
5 min readOct 25, 2023

How to optimize `swift-syntax` package compilation for a better performance of Swift Type Checker

Photo by Mathew Schwartz on Unsplash

Since many years I have been involved in dozens of iOS / macOS projects written in Swift. A large portion of my experience includes speeding up compilation duration by helping Swift’s Type Checker. Why I care for this is simple: I like iterations to be fast, in fact so fast, that implementing a screen would be a real pleasure, like working with SwiftUI Previews in Xcode or using InjectionIII.

Back in 2014 it was fun to start digging into the newly published Swift programming language, figuring out how to declare a class as compared to ObjC ❤️ and other interesting updates around iOS SDK. Starting 2017 I have participated in Swift projects either mixed with ObjC codebase, or just pure Swift, where I have observed simple functions, or even primitive calculations, throwing an infamous error:

The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions

Recently a Swift Forums thread was shared by one of the authors of TCA, that SwiftMacros contains a dependency on swift-syntax, which increases a clean build duration by 20 seconds. That got my interest, and given my work on SwiftCompilationTimingParser, I took it for a spin!

Problem Statement

When importing SwiftSyntaxMacros , Xcode adds swift-syntax as a dependency automatically, because SwiftSyntaxMacros is defined in swift-syntax’ Package.swift as follows:

...
.target(
name: "SwiftSyntaxMacros",
dependencies: ["SwiftDiagnostics", "SwiftParser", "SwiftSyntax", "SwiftSyntaxBuilder"],
exclude: ["CMakeLists.txt"]
)
...

swift-syntax-Package , which is the main target of swift-syntax project, gets compiled with a clean build within just 13 seconds on M1 Max with 32 GB RAM.

Setup of SwiftCompilationTimingParser is straightforward:

  1. clone the repository
  2. set the required arguments in Xcode’s scheme
  3. build & run

After building swift-syntax with the following command:

xcodebuild -scheme swift-syntax-Package \
ONLY_ACTIVE_ARCH=YES \
OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-expression-type-checking \
-Xfrontend -debug-time-function-bodies" \
-destination 'platform=macos' clean build clean \
| tee xcodebuild.log > /dev/null

I have processed the build log output with SwiftCompilationTimingParser, and the results were surprising to say the least: there are multiple functions and variables which compile for longer than 30 ms.

SwiftCompilationTimingParser produces JSON file with the “most wanted” symbols. Generated JSON can be processed and converted into CSV, which then can be used in the Numbers app:

Output of SwiftCompilationTimingParser processed as CSV

As depicted, there are multiple functions which can be optimized, since compilation of a function on average should be around 0.1–0.5 ms on M1 SoC¹. Would this optimization bring important speed up to the entire Apple Developer community? Let’s see…

[1] This observation is based on running xcodebuild build command with OTHER_SWIFT_FLAGS set to -Xfrontend -debug-time-expression-type-checking and -Xfrontend -debug-time-function-bodies on multiple projects using M1 Pro and M1 Max SoC.

Speeding Up Slow Functions

For better developer experience, working with Xcode source code editor is far easier than using report as it is. To highlight all problematic methods based on the similar approach as above, the following OTHER_SWIFT_FLAGS option can be set in swift-syntax Package.swift file:

for target in package.targets {
target.swiftSettings = target.swiftSettings ?? []
target.swiftSettings?.append(
.unsafeFlags([
"-Xfrontend", "-warn-long-function-bodies=30", "-Xfrontend", "-warn-long-expression-type-checking=30"
])
)
}

Where “30” is the amount of ms to be treated as threshold, which would eventually enable Xcode to show a warning if a method is slow enough.

The following methods were problematic for the Type Checker:

#1: isValidIdentifierContinuationCodePoint

// Computed variable which caused Type Checker to hang for around 123.67 ms 
extension Unicode.Scalar {
var isValidIdentifierContinuationCodePoint: Bool {
...
}
}

By applying “ugly code” approach, where complex expressions are split into local variables, explicitly annotated with types at all times, the compilation duration of this variable got reduced down to ~29.23 ms. This is a 76.4% improvement for this single use case.

#2: isSlice(of other: SyntaxText) -> Bool

Another example of slowly compiling code is the following method:

// Method which does specific comparision took around 299.29 ms to type check
public struct SyntaxText {
public func isSlice(of other: SyntaxText) -> Bool {
...
}
}

Yet again, “ugly code” approach helped:

public func isSlice(of other: SyntaxText) -> Bool {
// If either of it is empty, returns 'true' only if both are empty.
// Otherwise, returns 'false'.
guard !self.isEmpty && !other.isEmpty else {
return self.isEmpty && other.isEmpty
}
+ let first: Bool = other.baseAddress! <= self.baseAddress!
+ let second = UnsafePointer<UInt8>(self.baseAddress! + count)
+ let third = UnsafePointer<UInt8>(other.baseAddress! + other.count)
+ return (first && second <= third)
- return (other.baseAddress! <= self.baseAddress! && self.baseAddress! + count <= other.baseAddress! + other.count)
}

The newly baked ugly code gets processed by Type Checker within just 7.1 ms. This is a 97.7% improvement simply made by splitting expression into local variables.

Other Slow Compiling Code

Report also shows other functions which are slow to compile/type-check, but they cannot be addressed directly. I’ll publish Part 2 of this series, and you’ll be able to take a sneak peak into how generated files are working in swift-syntax, as well as you’ll find out if it’s possible to optimize the generated files for a faster compilation.

Conclusion

While it might seem like a great improvement in compilation speed, the actual yield is not that big of a deal on M1/M2 SoC, where measurements performed on M1 Max CPU with 32 GB RAM show the following output:

Though, it’s worth mentioning that Intel CPU might be very happy with these changes! I have created a PR and let’s see what would be the outcome!

Writing Swift code is amazing, static type checking, value types, generics and many other perks are built-in and are wonderful to (over)use as if to compare with the process of writing Objective-C code. Nevertheless, Swift’s Type Checker performance is still missing out on some optimizations, and is causing businesses and companies to lose substantial amounts of revenue annually due to its sometimes irrationally slow performance.

Want to Connect?

Follow me on X (Twitter): @r_alikhamov or LinkedIn 🤝

Resources

  1. SwiftCompilationTimingParser — https://github.com/qonto/SwiftCompilationTimingParser
  2. swift-syntax — https://github.com/apple/swift-syntax
  3. Swift Type Checker — https://github.com/apple/swift/blob/main/docs/TypeChecker.md

--

--