Speeding up your rust compiles, for both clean whole program compiles & incremental changes
Script for a video I’m working on…
Hey everyone, I’m Trent, and today we’re going to look at how to speed up your rust compile times. We’re going to go through 12* tips and techniques, random from least specific and least effort to most specific and custom.
Just want to start off by saying these are mostly compiled from other sources, links in description, and I’m also adding benchmarks of my own to show the impacts.
I’ll be benchmarking across 3 configurations. (Show computers).
An older desktop 4 core system, testing SSD vs HDD, and a newer mac pro 10 core with a nvme ssd. I’ll be running builds for a number of projects, testing both clean release compile times (building from scratch with optimizations, typical of CI), and incremental debug iteration times, so you’ve changed a line of code and want to see the result with that change.
I’ll be using a number of rust projects, mostly just ones I know or use, that are easy enough to build on both linux and mac.
Primer on rust compilation
By default cargo, the rust package manager, will orchestrate compilation of your project so that it can best utilise the number of available cores on the machine. It does this by analysing the graph of dependencies and arranging compilation so it can most effectively parallelise compilation. (nit cleanup dupe concept^)
When you run
cargo build in your terminal it may look like things are happening one after the other, but each of those lines is really just indicating that rustc has begun compiling it. That’s why it seems like the first bunch all happen at the same time, and why that first batch is suspiciously close to the core count of your machine.
You can easily check what this looks like for your project by building with
--timings and opening the corresponding output.
In this example we can see there was 56 compilation units,
We’ll cover compiler options later.
I want to quickly shout out cargo. Rust the language has a lot going for it, however cargo is absolutely a best in class tool, to call it a package manager just does not do it justice. It fetches your dependencies, orchestrates builds, runs tests, generates amazing documentation sites, allows you to configure builds in all kinds of ways, lints, benchmarks, build with different compiler versions, the list goes on and on. It’s simply incredible and makes working with rust an absolute dream.
It kind of makes me dread going back to non-rust projects. Just last week I tried to bump a minor typescript version in a large node project, and I spent a good few hours trying to resolve dependency issues between the test runner, the linter, the plugin that connects them, the plugins that connects both of them to typescript, the mocking library, demoralised, I eventually just gave up.
Coding is not just about what you type into your editor, it’s just as much your toolchain, how you build, how you test, how you run, and how you maintain software over time, and cargo nails it.
Upgrade your hardware.
This could almost go without saying, but it is the largest impact you can possibly have on your compile times with relatively minimal time or fuss.
There have been a couple of advancements in the last few years which have dramatically changed the landscape, namely solid state drives, especially nvme, core count explosions, and IPC or instructions per clock.
Let’s jump into some benchmarks.
(Benchmarks across systems)
Utilise your system effectively
If you’re on windows, check to see if windows defender is getting tripped when you compile. It likes to scan and analyse executables, including rustc and your output binary, and can easily tank performance. You can add an exception for your project folder or rust compiler executable in the settings.
Avoid disk or cpu usage, video recording or streaming, game launcher download, virus scans.
If you’re continually building, consider using cargo watch or bacon to avoid having to manually trigger a re-run. You can run both of them with the command of your choice, run, build, test, etc.
Do you need to build?
If you just want to know if your code would compile, you can run cargo check instead of cargo build and save time. This can be especially helpful in larger projects when linking time starts to creep up.
(a few benchmarks)
You can also configure the command used when you save your projects in editors like vscode.
By default it should be on check, but if you’ve been using build or clippy for extra lints consider switching back to check and running anything else periodically.
Linking is the process of taking all the compiled chunks of code and assembling them together, here it would be for an executable binary, aka your program.
One rusts awesome features is its extensive ecosystem of crates. It’s super simple to bring in a new dependency, however nothing is free, and we typically pay the price for it in build types, with linking often being the biggest bottleneck.
However there is good news here, there are a number of different options for linkers we can choose from, with a very recent entry absolutely blowing away the rest.
Lets look at benchmarks of our previous projects but this time with different linkers.
Mold stands for moder linker, and is made by rui euyama.
Tuning your compile settings
Just like most other compiled languages, rust has all kinds of settings and knobs you can tune to customise it exactly how you like.
If you’ve used rust already you’ll know about debug and release modes. Debug for faster compiles and extra debug information built in, and release for slower compiles and a higher performing binary.
What you might not know though is these are just presets. (Display cargo website with two presets)
You can also customise each of them if you have different preferences. For example you might want to enable debug information in your release builds for an easier time debugging crashes in a production environment, or enable optimisations in debug.
opt levels (benchmark)
Impact of LTO (benchmark)
Codegen units (?)
Release settings, profiles
Incremental compilation & Compile settings & Bevy nightly tricks, lto, debug, opt
Tip 6 - Trim your dependencies
Include less code (remove or have lighter dependencies(warp vs axum, serde vs miniserde,, macros, clean features (utf8/ripgrep?))
Cargo build –timings
Tip 6, Trim your dependencies.
Crates.io and it’s ecosystem of libraries is one of the best parts about rust. It makes it so easy to get up and running by bringing in high quality libraries with just a single line change that it can be really easy to over do it.
All of the dependencies you add will need to get compiled, even if you don’t end up using them. Doing some spring cleaning of your Cargo.toml to remove dependencies that you aren’t using anymore will always help.
Of the dependencies you do keep, it’s likely you aren’t using all of their functionality. That’s where rust Features come in.
Feaatures provide a way to achieve conditional compilation and control optional dependencies. If you’ve spend any time using rust you’ve already been using them. They power quite a few important function from OS specific code, like filesystem operations, to building your test code and enabling debug assertions.
If you take a look at any reasonably sized rust project you’ll probably find some dependencies without any extra information, some with extra features enabled, and even some that disable the default features.
Lets go over how features work in this context.
If nothing is specified on a dependency you’ll receive the crates default features. You can see which features are enabled by default in the depdencies Cargo.toml file, or from the features page on docs.rs. https://docs.rs/crate/regex/latest/features.
In this example we can see there are 4 features enabled by default. If you look at each of those features definitions, we see they include many of their own features as well.
It’s important to note here that rust features are intended to be additive, meaning including features will always enable more functionality, they won’t disable or remove functionality.
There are some notable exceptions, for example many async related crates will allow you to choose between tokio or async std support by enabling one or the other feature, but will fail to compile if you enable both.
You may have also noticed the std feature is a common sight in your dependencies. One of rusts biggest strength is that it’s trivial to disable the standard library, and with it any assumptions about having access to things like an operating system or a memory allocator. You’ll lose many of the conveniences of std like strings, hashmaps, and file operations, however you’re now able to write operating systems or target microcontrollers with all of rusts excellent ergonomics like borrow checking, iterators, sum types etc etc.
What this means is if you’re targeting a no_std environment, you can disable the default features of the crate and std by extension, and you’ll still be able to use the core logic of that crate without worrying about it doing anything silly like trying to allocate or open a file. (Show some encoding/decoding library on screen?)
The takeaway from all that, is check out your dependencies and see if they have any features enable you don’t need and get rid of them, or if they have features enabled by default you don’t need, you can build up just the set of functionality you require.
Some notable examples are tokio and regex.
Most crates that use features will describe which ones you’ll need to enable in their documentation, and modules will also specify their requirements .
You can see what features are enabled by running https://doc.rust-lang.org/cargo/reference/features.html#inspecting-resolved-features.
A nice bonus from anything you managed to trim here is that you’re lowering the load on tools like rust analyzer, which means a snappier editing experience and quicker boot times when you open your project.
Tip 7 - Dynamic linking
Dynamic linking (bevy, turn your own dependencies into dynamic, recent article on reddit?)
Tip 8 - Sub crates and features
Split into sub crates (compilation units?)
Iterate inside sub-crates or separate projects to avoid the overhead of your big project. (Examples)
Move specific parts that don’t always need to be there into features if it makes sense.
Tip 8, Sub crates and features.
As I said earlier, rust compiles crates at a time, not individual code files. As your project grows the time to compile just your code will as well.
If you’ve got modules cleanly separated this might be an easy win for iterative compile times. I
Tip 9 - Code specific changes
Code changes (less macros, prefer dynamic dispatch where possible (ripgrep example?))
Before you go running to change all your
Vec::new() hear me out.
Remove unused derives.
Update your compiler version, rustc team tracks and improves performance, eg recent windows 20% improvement from profile guided build
Compiler profiling information
For tests https://endler.dev/2020/rust-compile-times/#use-cargo-nextest-for-faster-test-execution
Hot reload? https://robert.kra.hn/posts/hot-reloading-rust/ sc
Rust analyzer competing with cargo
https://doc.rust-lang.org/cargo/reference/profiles.html#debug level 1 or 0 if not using
Optimizing sub- dependencies like bevy quick config does (less code to link/build??)
Rust compiler performance tracking