Compilation Benchmarks
This page compares the compilation performance with bon
and alternative builder crates.
For any proc macro, two things are contributing to compile times:
- Compiling the proc macro crate into a dynamic library.
- Compiling the code generated by the proc macro.
The benchmarks here intentionally include only 2
in measurements.
See the reasoning behind this
The proc macro crate dynamic library is compiled only once and remains in the build cache. For example, once you add bon
as a dependency, it'll be compiled only once and never recompiled during incremental rebuilds. The cost of that build is rather small and constant. So this isn't of a big interest to the scope of these benchmarks.
Measurements
The benchmarks measure the time it takes to run cargo build
against a crate that uses #[derive(Builder)]
for the given number of structs with the given number of fields in each of them.
Builder crate | 10 structs with 50 fields | 100 structs with 10 fields |
---|---|---|
bon | 2.096s | 2.340s |
typed-builder | 2.088s | 1.831s |
derive_builder | 0.449s | 1.026s |
no macros | 0.112s | 0.113s |
Summary
Builder macros do add noticeable overhead to the compilation performance due to the number of additional structs and methods they generate.
bon
has more compile time overhead than other crates, which is likely still reasonable, but let's understand why and what we get in return from this.
Why does bon
lose to typed-builder
in compilation perf?
bon
's generated code is a bit more complex than typed-builder
's, because bon
generates additional traits and structs to expose a nice and stable typestate API.
This is also done to reduce the builder's type signature size and reduce generic type noise in the generated builders' docs. See docs comparison (the difference is drastic).
Can bon
improve compile times?
Once the associated_type_defaults
nightly Rust feature hits stable, it'll be possible to greatly improve compile times. See this comment, which measures the improvement at 16-58%.
Any other improvements come at a cost 🪙. bon
prioritizes the convenience of the generated builder API, documentation cleanness, and higher level of compile-time checks by default.
If you'd like to improve your compile times, consider disabling compile-time checks for overwrites of optional members in setters with #[builder(overwritable)]
. However, don't rush with that, think carefully about the tradeoff you would make.
Why derive_builder
is fastest to compile?
The main contributor to compile times of code generated by bon
and typed-builder
is their usage of generics to represent the typestate. Builders generated by derive_builder
don't use any generics.
The main downside of derive_builder
's approach is that its finishing build()
method always returns a Result
, because it needs to validate, that all required fields were set at runtime.
Is this all worth compilation time overhead?
It depends on your use case.
If you are considering builders in your crate's public API, then it's definitely worth it. One of the main focus areas of bon
is breaking change prevention (i.e. Compatibility) and builder API ergonomics. This way you can provide stable, evolvable and convenient API for your users.
If you are considering builders in your private modules, then the main feature of bon
you'd be interested in is optional members and nicer syntax.
The general rule is that builders make more sense for pervasive structs and functions that you have to construct/call frequently. Don't blindly add builders to every other struct or function. If that struct or function is used only in a single place in your code, then using a builder for it may be unnecessary.
Hardware
The benchmarks were run on a dedicated root server AX51-NVMe
on Hetzner.
- OS: Ubuntu 22.04.4 (Linux 5.15.0-76-generic)
- CPU: AMD Ryzen 7 3700X 8-Core Processor (x86_64)
- RAM: 62.8 GiB
References
The source code of the benchmarks is available here.