Skip to content

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:

  1. Compiling the proc macro crate into a dynamic library.
  2. 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 crate10 structs with 50 fields100 structs with 10 fields
bon2.096s2.340s
typed-builder2.088s1.831s
derive_builder0.449s1.026s
no macros0.112s0.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.