Compiling Rust is testing
Edit: it looks like some people misunderstood what I wanted to express with this post. So, to avoid further confusion, pretty much the only thing that I wanted to say here was this: I know that waiting for a Rust program to compile sucks, but we should not forget all the benefits that this compilation process brings us in return. (Yes, this post could have been a tweet).
This post is a very short contemplation of Rust compilation (times). Don’t expect anything super insightful, just a bunch of thoughts that came up while I was listening to the Rustacean Station podcast.
I’m currently analysing the results of the Rust 2023 annual survey (full results coming Soon™, hopefully sometime in February) and as usually, one of the most common complaints is long compilation time of Rust programs. This is certainly not a surprise, of course, as compilation times are often being cited as one of the biggest frustrations that people have with Rust.
As a member of the Compiler performance working group, I’m constantly on the lookout
for approaches that could improve the speed of the Rust compiler. Currently, there are exciting
things on the horizon, like the parallel frontend and the Cranelift backend now being available on nightly
, and the lld
linker slowly moving to becoming the default linker on Linux. There are also various tricks that you can do to improve your compilation times on stable
right now. However, these things will take time, and even once they are all implemented and even used by default, and even after we make a lot of other improvements to the compiler, some programmers will still consider Rust to be too slow to compile. And they’re not wrong, since especially when compared to some other languages like Go or even Python, the compilation
speed of Rust might never be considered to be fast enough for all use-cases.
The codegen backend (LLVM), and sometimes the linker, is often being cited as a large source of slowness when compiling Rust, and truly so, since both can be bottlenecks. I’m personally putting a lot of hope into the Cranelift backend (or even a Rust interpreter?) and faster linkers (lld
or mold
), to improve Rust’s compilation speed.
However, it turns out that for cases that I consider to be latency-bound, where you want to perform the make a change -> run tests
development loop as quickly as possible, using a non-optimized (debug
) incremental build, the frontend of the compiler is often the true bottleneck (or, at the very least, it takes a non-trivial part of the compilation)1. Again, with the parallel frontend and other future improvements, this will get much better, but at the same time, there are some fundamental factors that just limit how fast the frontend can be. Be it general design decisions that the language made a long time ago, or the determination of the compiler to provide exceptional error messages, which introduces a non-trivial performance cost even in the happy path where there are no errors.
While thinking about this, I realized something that I haven’t explicitly thought about before, even though it’s kind of obvious.
If you’re used to e.g. Python, where you can make an incremental change and then rerun tests immediately, any compilation time might seem like pure overhead. In a way, I also had this mindset about Rust, since when I repeatedly perform the make a change -> run tests
loop, I have considered the compilation time to just be a delay before I can run my tests again.
But there’s also another way to look at this: The compilation itself is a part of your test suite! Since Rust can catch so many issues during compilation already, I think that it could be interesting to explicitly think about the compilation process being a first part of your overall test suite. Any interface (function signature, trait, variable type, …) being spelled out in the code is a mini unit test, and any compile error is said unit test failing. Notably, this also generalizes to the architecture of the program as a whole. Rust is notorious for guiding (sometimes even forcing) you towards certain architectural patterns, particularly around ownership of data, which can prevent annoying problems that could otherwise cause issues much later down the road. In a way, this could be seen as the compiler performing an integration test on your codebase during compiling, where it checks that the individual parts of your program compose together in a reasonable way.
Of course, this is not a new revelation by any means, as one of biggest cited advantages of Rust is that it helps with shifting left, i.e. moving many bugs from production (runtime) to development (compile time), and thus reducing the cost to fix said bugs. You could say that all languages that are statically typed and have a reasonable type system provide the same benefit. However, I would say that thanks to enums, pattern matching, ownership, destructive move semantics, lifetimes and other features, Rust “executes many more tests” during compilation than any other “mainstream” programming language, so to speak.
Well, that’s all that I wanted to express in this post. If you have any comments or thoughts, please let me know on Reddit.
-
We have recently started visualizing the ratio of time spent in frontend, backend and linker for all the Rust benchmarks that we execute. You can see it on the Perf.RLO compare page, if you expand a row in the benchmark result table and take a look at the colourful horizontal bar chart labeled
Sections
. ↩