Compile Times
Although this book is primarily about improving the performance of Rust programs, this section is about reducing the compile times of Rust programs, because that is a related topic of interest to many people.
Linking
A big part of compile time is actually linking time, particularly when rebuilding a program after a small change. It is possible to select a faster linker than the default one.
One option is lld, which is available on Linux and Windows.
To specify lld from the command line, precede your build command with
RUSTFLAGS="-C link-arg=-fuse-ld=lld"
.
To specify lld from a config.toml file (for one or more projects), add these lines:
[build]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
lld is not fully supported for use with Rust, but it should work for most use cases on Linux and Windows. There is a GitHub Issue tracking full support for lld.
Another option is mold, which is currently available on Linux and macOS. It
is specified in much the same way as lld. Simply substitute mold
for lld
in
the instructions above.
mold is often faster than lld. It is also much newer and may not work in all cases.
Incremental Compilation
The Rust compiler supports incremental compilation, which avoids redoing work
when you recompile a crate. It can greatly speed up compilation, but the
compiled binary may be larger and run more slowly. For this reason, it is only
enabled by default for dev builds. If you want to enable it for release
builds as well, add the following lines to the Cargo.toml
file.
[profile.release]
incremental = true
See the Cargo documentation for more details about the incremental
setting, and
about enabling specific settings for different profiles.
Visualization
Cargo has a feature that lets you visualize compilation of your program. Build with this command (1.60 or later):
cargo build --timings
or this (1.59 or earlier):
cargo +nightly build -Ztimings
On completion it will print the name of an HTML file. Open that file in a web browser. It contains a Gantt chart that shows the dependencies between the various crates in your program. This shows how much parallelism there is in your crate graph, which can indicate if any large crates that serialize compilation should be broken up. See the documentation for more details on how to read the graphs.
LLVM IR
The Rust compiler uses LLVM for its back-end. LLVM’s execution can be a large part of compile times, especially when the Rust compiler’s front end generates a lot of IR which takes LLVM a long time to optimize.
These problems can be diagnosed with cargo llvm-lines
, which shows which
Rust functions cause the most LLVM IR to be generated. Generic functions are
often the most important ones, because they can be instantiated dozens or even
hundreds of times in large programs.
If a generic function causes IR bloat, there are several ways to fix it. The simplest is to just make the function smaller. Example 1, Example 2.
Another way is to move the non-generic parts of the function into a separate,
non-generic function, which will only be instantiated once. Whether this is
possible will depend on the details of the generic function. When it is
possible, the non-generic function can often be written neatly as an inner
function within the generic function, as shown by the code for
std::fs::read
:
pub fn read<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
fn inner(path: &Path) -> io::Result<Vec<u8>> {
let mut file = File::open(path)?;
let size = file.metadata().map(|m| m.len()).unwrap_or(0);
let mut bytes = Vec::with_capacity(size as usize);
io::default_read_to_end(&mut file, &mut bytes)?;
Ok(bytes)
}
inner(path.as_ref())
}
Sometimes common utility functions like Option::map
and Result::map_err
are instantiated many times. Replacing them with equivalent match
expressions
can help compile times.
The effects of these sorts of changes on compile times will usually be small, though occasionally they can be large. Example.