Hello Swift Community,
I'm reaching out to share our team's recent experiences with the newly introduced Swift Macros feature, a promising addition we've been eagerly exploring. As part of our ongoing efforts to enhance code quality and reduce boilerplate, we conducted a series of experiments with Swift Macros to understand their impact on our development workflow and we found out a significant build time overhead, which became a blocker for adoption.
For example, we benchmarked compiling an example documenting how to adopt macros in SwiftUI. The result was a 2x increase in build times.
We are aware of an existing forum thread discussing macro build performance. However, a big part of it seems to be focussing on the overhead of building SwiftSyntax, which is not a primary concern for us as we are using pre-compiled SwiftSyntax. Hence a new thread.
Project Setup
SPM complete repros download link
We set up several medium-sized projects incorporating different workflows to gauge the performance of Swift Macros. Our experiments were structured around the following macro types:
StringifyMacro Project:
- Summary: The analysis contrasts a project utilizing StringifyMacro with one employing a non-macro equivalent.
- Refer to “WithStringifyMacro” and “WithoutStringifyMacro” folders in the above zip.
- “WithoutStringifyMacro” has files that make use of expressions like “print((1+2, "1 + 2"))”. “WithStringifyMacro” has files that make use of expressions like “print(#stringify(1 + 2))”. More details about this pattern can be found in the uploaded zip or here(with macro), here(without macro) for a quick reference.
- Source of StringifyMacro implementation: swift-syntax/Examples/Sources/MacroExamples/Implementation/Expression/StringifyMacro.swift at main · apple/swift-syntax · GitHub
MemberwiseInitMacro:
- Summary: The analysis contrasts a project utilizing MemberwiseInitMacro with one employing a non-macro equivalent where explicit initializers are added.
- Refer to “WithMemberwiseInitMacro” and “WithoutMemberwiseInitMacro” folders in the above zip.
- “WithMemberwiseInitMacro” has classes making use of “@MemberwiseInit(.public)” whereas “WithoutMemberwiseInitMacro” has the exact same setup but with explicit initializers in the same file scope. For quick reference of exact usage, refer to these links: with macro usage, without macro usage.
- Source of MemberwiseInitMacro implementation: GitHub - gohanlon/swift-memberwise-init-macro: Swift Macro for enhanced automatic inits.
ObservationMacro:
- Summary: Utilizes the example provided during WWDC, we compared its performance against the conventional ObservedObject pattern, as talked about it in the migration guide here - Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation
- This was a motivating example to try as the above guide calls out: “Tracking optionals and collections of objects, which isn’t possible when using ObservableObject.” so we would like to get such benefits with Macros but looks like inviting the Observable macro equivalent comes with ~2x the build time overhead. (the project uses it in the exact same way as suggested in the guide)
- Refer to “WithObservationMacro” and “WithoutObservationMacro” folders in the above zip.
- For quick reference of exact usage, refer to these links: with macro usage, without macro usage.
Findings and Concerns
Our tests revealed a significant increase in build time overhead when macros were employed.
Clean Build Times Comparison (Avg of 3 runs)
- Explicit Initializers vs MemberInitMacro
- Without Macro (Target ST0): 226.0 seconds
- With Macro (Target ST0): 429.0 seconds
- ObservedObject vs ObservationMacro
- Without Macro (Target ST0): 84.8 seconds
- With Macro (Target ST0): 154.9 seconds
- Explicit Expressions vs StringifyMacro
- Without Macro (Target ST0): 42.5 seconds
- With Macro (Target ST0): 107.3 seconds
Notably, the Activity Monitor indicated a substantial load attributed to swift-plugin-server among other macro-related processes. We hypothesize that a part of the build time overhead is primarily due to the invocation of macro executables as an additional build step – a contrast to the integrated compilation process observed in languages like Rust and Kotlin. To test this, we also wrote the simplest possible macro; one that takes no arguments, and returns an empty string literal. It had the same overhead.
Addressing Build time concerns
While we are excited about the potential of Swift Macros to modernize our development practices, the observed build time overhead currently poses a substantial blocker to their adoption in our projects.
We are reaching out to the community for insights:
- Are there known workarounds or optimizations that we might not be aware of that could mitigate this overhead?
- Is there ongoing or planned work within Swift to address these concerns?
- Are there easy wins that we can help to implement?
We look forward to any guidance, suggestions, or discussions that could help us leverage Swift Macros more effectively while maintaining our project's performance standards.