Nicholas Nethercote, a Rust compiler contributor, writes about measuring and speeding up rustc and other Rust performance work.
Commenters like his detailed, step-by-step optimization write-ups and cite his Rust Performance Book and logs of incremental compiler speedups.
for context: in rust the "translation unit" is a crate (in C++, each source file is its own translation unit). So you only get parallelism across crates when compiling in rust. When you have a single-crate project, this means that
1. you get parallelism across all your dependencies, but
2. your (final) crate is serial.
Splitting your crate can therefore get you parallelism back in step 2, which can be a (compilation) perf win. You can also get a caching benefit when there aren't changes, but even absent that you can get wins.
It can come at the cost of runtime performance though, as rust won't (by default) do things like inline across translation units iirc. You can get back some of this perf back via various tricks (for example link-time optimization).
Looks like the rust performance book has some writing on these tradeoffs. I haven't read these articles, but the table of contents on the left seems reasonable enough.
https://nnethercote.github.io/perf-book/build-configuration.html
Again I haven't read that (so I'm assuming it says the following), but you can get "best of both worlds" by configuring debug builds to not do LTO (= faster compilation speed, slower runtime), and release builds to do LTO (= faster runtime, slower compilation speed). There are also variants of LTO that make various tradeoffs you could look into.
That article also links to the `cargo-wizard` subcommand of cargo
https://github.com/Kobzol/cargo-wizard
it won't auto-split crates for you (of…
> Is it an unpopular opinion to say 75% of “a lot” is still “a lot”
I don’t know if it’s unpopular, but it may be inaccurate. OP claims that they improved by 75%, so you probably meant “25% of a lot is still a lot”.
> I would expect incremental build speedup to be prioritized, and steadily improving. But clearly it isn’t moving very fast in that direction. So why not?
Is this assessment based on something?
For what it’s worth, people have been working on it for years, with varying approaches and levels of success. The recent spate of articles you’ve seen is precisely because these approaches are bearing fruit. Several folks have worked on making a previously single threaded frontend multithreaded, others have added an option to swap the LLVM backend out for a much faster one (cranelift), and yet others are working on moving to a faster linker by default.
The net gain from all of these once they land could be as much as 30–50%, depending on the project, mode of compilation (clean or incremental) and hardware (single/multiple cores). That is a pretty fantastic improvement, considering the whole year improvements in the last 4 years have been 7%, 17%, 13% and 15%, for an overall speed gain of 37%.
If all of this doesn’t sound impressive, I’d encourage you to read nnethercote’s annual reports which explain how much effort went into making compile times faster. 2023’s report - …
The compiler itself is fine and its performance well managed, with weekly perf triage and efforts driven by profiling[1], but because adding dependencies is effortless and vetting them isn't, dependency trees tend to get large if not careful. In particular, widely used proc-macros (code transformations pulled by popular packages like serde, clap, tokio…) add very heavy dependencies (like syn) to the critical path. As a consequence, downloading and installing a tool from source is often a poor experience.
As far as development experience, cargo clippy / cargo check / rust-analyzer give you the feedback you need without involving codegen (except for any proc-macros in your dependency tree), and incremental compilation makes codegen faster after the first build. Work is also ongoing[2] to make debug-quality codegen faster.
Here is a quality article on the art of keeping a Rust project fast to build:
https://matklad.github.io/2021/09/04/fast-rust-builds.html
The overall series on organizing a large project is worth reading as well:
https://matklad.github.io/2021/09/05/Rust100k.html
[1]: https://nnethercote.github.io/2021/11/12/the-rust-compiler-has-gotten-faster-again.html
[2]: https://bjorn3.github.io/
I've been thinking a lot about a solution to Rust's notoriously slow compile times. From my research, it's not the borrow checker that is the issue, it's the heavy optimizations and monomorphization, and macro expansion [0]. There are efforts to improve Rust compile times, but I realized recently that it will never be lightning fast, so we (Rust users) should really be developing more scripting languages and DSLs that we can put all of the fiddly bits into.
Places where we would benefit the most from this is in the Games and UI space. I know game devs have already started by integrating lua, like with mlua [1]. In the UI space i think Makepad is the best example of a team making a dedicated DSL that can be hot-reloaded [2].
I think we need more of this! Go make a DSL next time you feel crushed by the weight of compiling Rust crates!
---
[0] and by my research i mean Claude. this is a great blog with many posts about improving compile times https://nnethercote.github.io/
[1] https://crates.io/crates/mlua . I don't have a reference for a project using it though so please reply if you know of one!
[2] https://github.com/makepad/makepad
I've tried AST memory layout work in the Zig style, look for "shrinkage" in these two posts:
- https://nnethercote.github.io/2022/10/27/how-to-speed-up-the-rust-compiler-in-october-2022.html
- https://nnethercote.github.io/2023/03/24/how-to-speed-up-the-rust-compiler-in-march-2023.html
It's hard work. Small AST changes often require hundreds of changes to the code. The required changes usually make the AST less ergonomic to work with. And the perf benefits I obtained were very small. Even after shrinking `ast::Expr` (by far the most common AST node kind) from over 100 bytes to 64 bytes on 64-bit.
The linked Zig PR has very impressive reductions in walltime/cycles/etc. but if you read closely it's restricted just to parsing, which is usually a very small slice of compile time, at least for rustc. My experience with these kinds of changes was disappointing. I concluded "I’d love to be proven wrong, but it doesn’t feel like this work is taking place in a good part of the effort/benefit curve."
(5) is pretty irrelevant, ABI isn't language.
You can strap basically any language on top of the C ABI. Many contender languages either use it natively or offer low-cost/no-cost C ABI bindings, specifically as its the lingua franca ABI.
You can check (5) off your list if your choice of replacement language has easy C ABI interop.
[edit]
(1) isn't particularly relevant either. These days the tooling for detecting memory leaks, data races, bugs, etc, operates at the debug info (DAWRF/dSYM) level. It doesn't matter what compiler or input language generated the debugging information, the tools should work just fine. You can run valgrind on your Rust binary for instance. [1] Is there any such tooling that specifically depends on the input source being C? I guess some static analysis tools?
[1] https://nnethercote.github.io/2022/01/05/rust-and-valgrind.html