Description
Problem
I was just using flamegraph to profile a clean cargo build
and observed that more than 4% of time was being spent parsing rustup configuration files. For a clean cargo check
it was about 7%.
This seems to be because when you run cargo
, it invokes rustup which then figures out which actual cargo binary to invoke (fine so far), then cargo invokes rustc
many times and each time it does so, it's invoking rustup again, which has to go and reparse configuration files to figure out which rustc binary to invoke.
See rustup_init::main
on the right (7.54%)
This was produced by running:
sudo apt install mold
cargo install flamegraph
git clone https://github.com/ebobby/simple-raytracer.git
cd simple-raytracer
cargo clean
flamegraph -- mold -run cargo check
To see the potential savings of bypassing rustup, we can set the RUSTC
environment variable to point directly at rustc.
With rustup:
hyperfine --prepare 'cargo clean' 'cargo check'
Benchmark 1: cargo check
Time (mean ± σ): 7.029 s ± 0.046 s [User: 22.295 s, System: 2.782 s]
Range (min … max): 6.939 s … 7.107 s 10 runs
Bypassing rustup:
RUSTC=`rustup which rustc` hyperfine --prepare 'cargo clean' 'cargo check'
Benchmark 1: cargo check
Time (mean ± σ): 6.418 s ± 0.040 s [User: 19.979 s, System: 2.409 s]
Range (min … max): 6.347 s … 6.481 s 10 runs
So about a 9.5% speedup. I'd expect for crates with relatively few, large dependencies, the time spent by rustup would be less as a percentage. For crates with lots of small dependencies it could be more.
For a trivial (hello world) binary, a warm cargo check
with a single line change is 101.5ms. If we set RUSTC
to bypass rustup, this drops to 67.6ms.
The above times were all on my laptop. For an extra datapoint, I tried building nushell on a relatively powerful desktop with lots of RAM and CPUs. Cold cargo check
went from 23.061 s ± 0.169 s to 22.313 s ± 0.188 s (3.4% speedup). A warm cargo check
(with trivial one line change) went from 613.6 ms ± 3.6 ms to 582.3 ms ± 7.4 ms (5.4% speedup).
Steps performed by cargo to determine what rustc
to run:
- The value of the environment variable
RUSTC
if that's set - The value of
build.rustc
from.cargo/config.toml
if set - placeholder for extra step added by proposed solution and most alternatives
- Just run
rustc
and find it from$PATH
Other tools (e.g rustdoc
) follow the same pattern.
rustc
is actually a little different in that steps 1 and 2 are first performed for rustc_wrapper
and rustc_workspace_wrapper
. This doesn't affect the proposed solution or the alternatives.
Proposed solution
Add step 3: Use tool (e.g. rustc
) from the directory that contains the current cargo
binary in preference to using PATH
.
Draft commit - still needs testing, updating of tests etc.
The main change to behaviour, would be a scenario like the following:
- A rust toolchain exists in
/aaa
- A rust toolchain exists in
/bbb
PATH=/aaa:/bbb
orPATH=/aaa
- i.e./aaa
is the first or only toolchain on the path.- User runs
/bbb/cargo build
- Before,
/bbb/cargo
would have invoked/aaa/rustc
because that's what's on the PATH - After the proposed change,
/bbb/cargo
would invoke/bbb/rustc
While any change has the potential to break someone, the new behaviour actually seems less surprising to me. From the perspective of someone who only uses cargo
, rustc
could be considered an internal implementation detail and one might expect invoking cargo
via an explicit path to use the associated rustc
and other related tools.
Alternatives considered
Alternative 1 - make rustup faster
This alone probably doesn't get us all the speedup we'd like. Even if we sped up rustup by a factor of 2, we'd still be spending a significant amount of time once we invoke it once for every rustc
invocation.
Alternative 2 - change rustup to set the RUSTC
environment variable
This would break crates that set build.rustc
in .cargo/config.toml
since the value in RUSTC
would override it. So this isn't really an option.
Alternative 3a - rustup sets new environment variable that's then used by cargo
Rustup could set DEFAULT_RUSTC
, which would be like RUSTC
but would, if set, be used at step 3.
A downside is that this option treats rustc differently to the other tools that rustup wraps - unless we also add environment variables for other tools as well. e.g. DEFAULT_RUSTDOC
- but that is messy and verbose.
Alternative 3b - environment variable is the directory containing the tools
Similar to alternative 3a, but instead of having the new environment variable point to RUSTC, have it point to the directory. This would allow all tools to be treated consistently.
One downside of this option (and 3a) is that if the user bypasses the rustup wrapper by explicitly invoking cargo, then cargo would revert to invoking rustup
for every call to rustc
.
Alternative 4 - cargo could run rustup which rustc
once
If cargo ran rustup which rustc
, it could then (if the command succeeded) use the result at step 3.
We'd still be running rustup
twice - once to determine which cargo
binary to invoke, then again within cargo
. It's a lot better than invoking it N times though.
Notes
No response