Skip to content

Bypass rustup wrapper when invoking rustc #10986

Closed
@davidlattimore

Description

@davidlattimore

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.

flamegraph

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:

  1. The value of the environment variable RUSTC if that's set
  2. The value of build.rustc from .cargo/config.toml if set
  3. placeholder for extra step added by proposed solution and most alternatives
  4. 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 or PATH=/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-feature-requestCategory: proposal for a feature. Before PR, ping rust-lang/cargo if this is not `Feature accepted`

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions