Skip to content

Latest commit

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

rust-performance

Patches and measurement harness for making cargo check faster, plus the measurements that justify each one and the ones that killed the ideas that did not work.

The headline: a cold cargo check of rust-analyzer from an empty target directory went from 23.77 seconds to 0.47 seconds on an Apple M4.

projectunitsbaseline coldcached coldcargo's no-op floorspeedup
rust-analyzer23923.77s0.47s0.10s50.6x
ripgrep465.37s0.18s0.03s29.8x
tokio404.66s0.17s0.04s27.4x
solo (zero dependencies)13.28s0.09s0.01s36.4x
clap231.54s0.15s0.04s10.3x

The no-op column is what cargo costs when it decides an already-built directory needs nothing. That is the floor any caching approach can reach.

Full write-up, including the four ideas that failed and why: https://www.drmhse.com/posts/cutting-a-cold-cargo-check-from-24-seconds-to-half-a-second/

Repository layout

pathcontents
patches/the two patches, the upstream commits they apply to, and what each new file does
harness/the benchmark and correctness scripts, all driven by environment variables
experiments/standalone reproductions of single findings, plus the generators they need
results/every measurement, with the hardware and compiler that produced it

Branches carrying the same work, ready for review or a pull request:

repositorybranch
https://github.com/drmhse/cargorust-performance
https://github.com/drmhse/rustrust-performance

What the patches add

Four unstable flags, across two repositories.

flagrepowhat it does
-Zshared-cachecargoreuses built units from a machine-wide cache, keyed by everything that determined the output
-Zadaptive-threadscargogives each rustc invocation a share of the machine based on how many units can run alongside it
-Zinterface-hashcargo, rustcskips re-checking a crate when its dependencies' public interfaces are unchanged
-Zearly-metadatacargo, rustcemits metadata before body checking, so dependents can start sooner

-Zshared-cache and -Zadaptive-threads work against a stock nightly rustc. -Zinterface-hash and -Zearly-metadata need the rustc patch as well.

Applying the patches

Both patches are against specific upstream commits, recorded in patches/BASE.txt, and are verified to apply cleanly to them.

git clone https://github.com/rust-lang/cargo &&cd cargo
git checkout 2305ac97ea879ad32ea0dd6366b5f8dad1ba4ce7
git apply /path/to/patches/cargo.patch
RUSTUP_TOOLCHAIN=nightly cargo build --release

The cargo repository's MSRV runs ahead of stable, so the nightly toolchain is required to build it.

git clone https://github.com/rust-lang/rust &&cd rust
git checkout eab115ea6d842276c6ad7b819e08297c8e7693f0
git apply /path/to/patches/rustc.patch
./x.py build --stage 1 compiler/rustc library

Building compiler/rustc on its own leaves stage 1 without a standard library, so both targets are needed on one line.

Running the harness

Paths come from the environment, so the same scripts run against a patched toolchain or a stock one:

export PATCHED_CARGO=~/src/cargo/target/release/cargo
export PATCHED_RUSTC=~/src/rust/build/aarch64-apple-darwin/stage1/bin/rustc
export BENCH_ROOT=/tmp/rsbench
harness/setup.sh # clone the four test projects, generate the fifth
harness/cold-build.sh # the headline table
harness/cache-diff.sh # differential correctness for the cache
harness/interface-diff.sh # differential correctness for the cutoff
harness/thread-scaling.sh # -Zthreads scaling on one cold compilation
harness/unit-timeline.sh # per-unit concurrency profile of a cold build
harness/additive-edits.sh # how many crates re-check per kind of edit

The two differential scripts are the ones that matter. Each runs a sequence of edits, wipes the target directory, and compares exit codes against an unpatched cargo, including deliberately broken code that has to be reported. A single edit followed by a single check cannot expose an artifact set that disagrees with itself, which is how a real bug survived a long run of green benchmarks.

Reading a result

Every timing script prints the number of rustc invocations alongside the seconds, because the count is what explains the time. A cached build of rust-analyzer that takes 0.47 seconds compiles zero units, so the remaining time is cargo deciding that 239 units need nothing, and the way to make it faster is to reduce cargo's per-unit overhead rather than the compiler's.

The same reasoning applies in reverse. During development the cache reached a state where nothing compiled and the build still took 3.27 seconds, which located the bug precisely: 153 of 235 units were being marked dirty by a dependency whose restoration cargo had not yet seen.

Reproducing a specific finding

experiments/macro-expansion-cost.sh compiles 16,000 lines of items twice, once written out literally and once produced by a macro_rules! macro, which is what showed that transcription runs at roughly parse speed and there is nothing there to cache.

experiments/generate-solo-crate.py writes the zero-dependency crate. It exists because every other test project mixes the cost of its own code with the cost of its dependency tree, and those respond to different changes.

experiments/summarise-timeline.py turns a per-unit timing log into the concurrency profile, which is what showed that a cold build runs at 10-way parallelism for its first 14 seconds and at 1 for the remaining 28.

What is deliberately switched off

src/compiler/fingerprint/read_set.rs implements per-item interface comparison, which makes the cutoff survive an added pub fn: 31 units and 6.90 seconds become 1 unit and 0.25 seconds. It is unsound, because skipping a dependent when the dependency's interface genuinely changed is exactly the case that metadata pinning cannot repair. It sits behind __CARGO_INTERFACE_ITEMS so the measurement can be reproduced, and is off by default.

The bugs worth knowing about

Four defects shaped the final design, and each is a trap that any similar attempt would hit.

A cache key over a local crate has to be transitive. Cargo's fingerprint for a local unit records which dependencies it had rather than what they contained, so changing a function signature in a leaf crate left every crate above it with an unchanged key. Caught by changing to_camel_case in rust-analyzer's stdx, wiping the target directory, and watching the build succeed with zero errors.

Hard-linking into a cache makes the cache mutable. The entry and the build directory's file become one inode, so anything that rewrites that file in place rewrites the cache with it. Storing copies and restoring links, which is safe because rustc replaces its outputs by rename.

Restoration has to run bottom-up. Cargo prepares a unit before it recurses into that unit's dependencies, so a unit's fingerprint is computed while its dependencies' outputs are still absent, and it memoises them as stale.

rustc stamps each dependency's crate hash into the metadata it emits. A crate skipped by the cutoff keeps metadata naming a hash that no longer exists on disk, and nothing notices until a later edit forces one of those crates to compile. The repair keeps the previous metadata when a unit's interface is unchanged and its dependencies' hashes are unchanged; the second condition is required, because restoring one crate's old metadata while its dependency has a new hash moves the same problem one level up.

Limitations

The first build of code that has never been built anywhere still costs full price. The gains come from CI, fresh clones, wiped target directories, branch switching, and projects that share dependencies.

The cache is machine-local on purpose. A build script can read a system library or an environment variable that cargo never fingerprints, so its result is only reliably reusable where those are the same.

solo compiles in 2.55 seconds of CPU, so perfect scaling across 10 cores would be 0.26 seconds, and rustc's parallel frontend currently reaches 1.16 by burning 1.9x the CPU. Closing that gap means making the frontend scale.

Measurement environment

Apple M4, 10 cores, 16 GB, macOS 15.7.2, rustc 1.100.0-nightly (67854e511 2026-08-15). Numbers taken on other hardware should say so; the ratios travel further than the absolute times.

About

Patches and measurement harness that take a cold cargo check of rust-analyzer from 23.77s to 0.47s

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages