Vastly improved recompile times in Rust with nightly features

184 points Koranir

There's been some talk on this subreddit about using the cranelift codegen backend to speed up compile times in Rust. It does speed the compiler up, but at the cost of generating worse code.

Instead, we can use another nightly feature - the codegen-backend cargo feature, which you can enable by adding cargo-features = ["codegen-backend"] to the top of your Cargo.toml:

cargo-features = ["codegen-backend"]

[package]
# snip

We can use this feature just like any other option in the [dependencies] section of out Cargo.toml:

[profile.dev]
codegen-backend = "cranelift"

And just like most other options, this can be selectively overridden for dependencies:

[profile.dev.package."*"]
codegen-backend = "llvm"

However, the generated code is still slow, as no optimizations are being applied. We can also override dependency opt-levels, like so:

[profile.dev.package."*"]
opt-level = 3

This will slow down the initial compile a lot, but you only compile dependencies once every while so the tradeoff is well worth it.

This by itself will speed up incremental compiles, but we can go further - by using the parallel rustc frontend and using a better linker.

For this, we'll have to move out of the Cargo.toml and into the cargo config. Its location is in $CRATE_DIR/.cargo/config.toml, where $CRATE_DIR is the same directory the Cargo.toml is in.

From there, we can set up different build overrides for specific targets, each under a [target.<target name>] category.

For example, here is the category for x86\_64 GNU/Linux builds:

[target.x86_64-unknown-linux-gnu]

We can change the linker rustc uses through the following configuration:

[target.<target name>]
linker = "clang" # Preferably the direct path to clang
rustflags = [
    "-Clink-arg=-fuse-ld=<linker>"
]

linker = "clang" is required to perform the override as it supports the -fuse-ld argument, which is supplied by the rustflags.

For Linux, mold is the best-in-class linker, but others would be lld.

Here's an example config for my Linux machine:

[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = [
    "-Clink-arg=-fuse-ld=/usr/bin/mold"
]

Especially on Linux, where the default linker is quite slow, this can massively improve recompile times.

To enable the parallel rustc frontend, we can add the -Zthreads=<num threads> flag to out rustflags:

[target.<target name>]
rustflags = [
    "-Zthreads=0" # Automatically detects the number of threads to use
]

All combined, that gets us:

Cargo.toml:

cargo-features = ["codegen-backend"]

[package]
# snip

[profile.dev]
codegen-backend = "cranelift"

[profile.dev.package."*"]
codegen-backend = "llvm"
opt-level = 3

.cargo/config.toml:

[target.<target name>]
linker = "clang"
rustflags = [
    "-Clink-arg=-fuse-ld=<linker>",
    "-Zthreads=0",
]

(e.g. on my machine:)

[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = [
    "-Clink-arg=-fuse-ld=/usr/bin/mold",
    "-Zthreads=0"
]

This ends up with a configuration that gives us lightning-fast recompiles for a decently-optimized executable (especially with heavier dependencies), at the cost of a higher initial compile.

Build timings for a simple project using \iced\\:

Configuration Build Type Time
Default From Scratch 23.6 seconds
Modified From Scratch 55 seconds
Default Incremental 2.5 seconds
Modified Incremental 0.6 seconds

In the default configuration, \iced\\'s software renderer struggles in debug modes for medium-sized windows, but the modified configuration has smooth performance until very large windows.