Good Friday to you all! Recently landed in nightly is the ability for Cargo to execute rustc in a “pipelined” fashion which has the promise of faster build times across the ecosystem. This support is turned off by default and the Cargo team is interested to gather more data and information about this feature, and that’s where you come in! If you’re interested in faster compiles, we’re interested in getting your feedback on this feature!
To enable pipelined compilation in Cargo, you’ll need at least nightly-2019-05-17
(today’s nightly):
$ rustc +nightly -V
rustc 1.36.0-nightly (7d5aa4332 2019-05-16)
$ cargo +nightly -V
cargo 1.36.0-nightly (c4fcfb725 2019-05-15)
After doing so you can set CARGO_BUILD_PIPELINING=true
or configure the build.pipelining = true
key in .cargo/config
:
$ CARGO_BUILD_PIPELINING=true cargo +nightly build
What is “pipelined compilation”?
Cargo today builds a DAG of crates to build whenever you execute cargo build
. This represents the dependency graph between crates and Cargo will execute rustc whenever all of its dependencies for that compliation have completely finished. The compiler, however, typically doesn’t have to fully wait for a dependency to finish compiling before starting the next one. In many cases all we need is “metadata” produced by the compiler to start the next compilation, and metadata from rustc is typically available much earlier in the compilation process.
With ASCII diagrams, let’s say we have a binary
which depends on libB
which in turn depends on libA
. A compilation today might look like this:
meta meta
[-libA----|--------][-libB----|--------][-binary-----------]
0s 5s 10s 15s 20s 30s
With pipelined compilation, however, we can transform this to:
[-libA----|--------]
[-libB----|--------]
[-binary-----------]
0s 5s 10s 15s 25s
So by simply tweaking how we call rustc we’ve shaved 5s off this compile! The exact intricacies of how this all works is somewhat complicated, but you can also find some more notes on the compiler-team
repository if you’re interested for more information! The general gist is that when Cargo compiles more than one crate it may be able to start rustc sooner than it does today, and if your build machine has enough parallelism this could mean faster build times.
When is pipelined compilation faster?
On the PR which implemented pipelined compilation in Cargo we did some loose measurements here and there, but we’ve unfortunately at this time been unable to find enough compelling use cases for this feature to justify the support in rustc and Cargo. That’s where you can come in though to help us gather more data!
In general it’s unlikely that pipelined compilation will provide an order of magnitude speedup across the board. Rather a few ingredients are necessary for pipelined compliation to really shine:
- The compiler must be called at least twice. If you’re only editing your leaf rlib, then pipelining doesn’t matter since there’s only one
rustc
instance. - The best wins will come from
--release
mode. Metadata in the compiler is currently produced just before translation to LLVM, optimization, and codegen. If metadata is produced 10ms before compilation finishes it’s not much of an opportunity to pipeline, but if metadata is produced 15s before compilation finishes then that could be 15s saved! Optimization typically takes longest in release mode. - Your machine needs to have idle parallelism. If Cargo could spawn rustc but your machine is already full on work doing other things, then there’s no benefit to spawning rustc sooner since it’d simply wait for the previous rustc to start sooner anyway. Most Rust builds tend to be on multicore machines, however, and tend to have available cores towards the end of compilation.
- Full crate graph builds may not see much benefit. We can’t pipeline all possible compilations due to details like procedural macros, build scripts, linked executables, etc. For these compilations Cargo still has to wait for rustc to previously completely finish before proceeding.
Given all that, the real use case for pipelined compilation is you’re incrementally compiling a project in release mode on a beefy machine where the incremental changes are a few crates down from the final product.
If you see benefits, though, we’re curious to hear about other use cases!
What measurements do we want?
The general idea of what we’d like to see is toggling CARGO_BUILD_PIPELINING=true
in some scenarios and seeing if compilation is faster with pipelining enabled. Some ideas are:
Full crate graph
$ cargo clean && cargo build
$ cargo clean && CARGO_BUILD_PIPELINING=true cargo build
and release mode
$ cargo clean && cargo build --release
$ cargo clean && CARGO_BUILD_PIPELINING=true cargo build --release
On CI
Try enabling CARGO_BUILD_PIPELINING
in nightly jobs on CI (e.g. Travis/AppVeyor/Azure Pipelines) and see if builds are faster.
Incremental Builds
Make a change to a crate (a few crates down in the dependency graph) and then measure before/after using CARGO_BUILD_PIPELINING=true
Other timing information
If you’ve got other scenarios you think might be worthwhile, let us know! If you can describe your scenario below, that’d also be great!
I’d like to get more involved!
Feel free to hop into into the wg-pipelining zulip channel for this topic, and we’d love to chat!