Skip to content

Latest commit

 

History

History
506 lines (405 loc) · 37.6 KB

README.md

File metadata and controls

506 lines (405 loc) · 37.6 KB

Benchmarks

tinymold exists to make Rust builds faster and smaller, so every claim it makes has to come with a number anyone can reproduce. This directory holds the harness, the corpus it builds, and the results it has produced.

Each benchmark measures tinymold against plain cargo, sccache, or both, so together they are the baseline every later phase has to beat:

  • commands: cargo build, clippy, test and llvm-cov, per mode, cold (empty target and cache), then warm (target deleted, cache kept). Results in COMMANDS.md; see commands.py.

  • scenarios: the full edit-loop matrix, and the main scoreboard. For every corpus project and every mode (baseline, sccache, and tinymold once its rustc wrapper exists), it runs eight cases:

    • a clean build;
    • a no-op rebuild;
    • a comment edit;
    • a private function body edit;
    • a public signature edit;
    • a second worktree of the same commit;
    • a branch switch A→B→A;
    • cargo test --no-run.

    For each case it records wall time (median, min, max), link time from a tiny timing linker wrapper the harness installs, units compiled versus fresh (from cargo build -v), target/ bytes split by kind, and cache hits.

  • dedup: builds two worktrees of each project and reports how much of the second's target/ is byte-identical to the first's at three levels: whole files, rlib members, and DWARF sections split out of object files. The gap between levels is what chunk-level storage could save beyond whole-file dedup.

  • baseline: plain cargo, default linker, no cache. Per corpus project: a clean build, a no-op rebuild, a one-line edit, an mtime-only change to every source file (what a branch switch or a cache restore does to cargo), building the tests, and a clean build in a second worktree at another path. It also measures target/ size and how many of the second worktree's artifact bytes are identical to the first's, which is what a shared store would dedupe.

  • cache: clean builds with a compile cache. No cache, then an empty cache (the dry run that fills it), then the warm cache (the cached run), then the warm cache from a second worktree at another path. The cache is sccache, today's reference, which tinymold's cache has to beat on hit rate and wall time.

  • link: the edit loop per linker. Append a comment to one source file and rebuild, once per linker available on the machine. That edit recompiles the leaf crate and relinks the whole binary, and on ripgrep about half of that rebuild is the linker (research).

  • cas: the cost of a cache hit. Restore one large artifact into a fresh path: a plain byte copy against tinymold cas get by reflink, hardlink, and copy. A hit only beats a rebuild if restoring doesn't copy bytes.

  • hit: the cost of a cache hit on units with many outputs. Every source file is touched and the project rebuilt through the wrapper, so each workspace unit is a hit. Incremental units with unpacked split debuginfo keep one object file per codegen unit, so restoring one places hundreds of files. --against <rev> builds another revision's tinymold and alternates the two on the same store; TINYMOLD_TRACE times each restore apart from cargo.

  • ci: a fresh CI runner at the same path, running cargo test --no-run. It compares four setups:

    • a cold build;
    • target/ restored from an archive with every source touched, as a target/ cache such as Swatinem/rust-cache does;
    • tinymold's store restored from an archive (the GitHub Action's default backend);
    • tinymold fed only by a local bazel-remote container (the remote backend).

    Downloading an archive from GitHub is not timed, so its size is listed instead. On bat the tinymold modes trail the target/ cache. Cargo re-runs every build script in an empty target/, and bat's compile C code, which RUSTC_WRAPPER never sees.

Results

Generated by bench/report.py from the JSON files in results/. Each heading lists the machine and toolchain the numbers came from. A number without its machine is not a result.

Cache per command

Build, clippy, tests and coverage, cold and then warm from the cache with the target directory deleted: see COMMANDS.md.

Edit-loop scenarios (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 3 runs, 2026-10-14)

Pinned: cargo build jobs: 18; target filesystem: apfs; linker: cc, rustc's default, behind a timing wrapper; linker threads: linker default; the platform cc linker takes no thread flag; output reuse: rustc default: each link writes a fresh output; incremental: on for baseline; off under sccache, which refuses it.

Each cell: median wall time, then median link time, then units compiled (cargo's count: a unit served from a cache still shows as compiled). Under sccache and tinymold, hits counts rustc invocations served from the cache across all runs. Under tinymold, cargo runs with RUSTC_WRAPPER=tinymold, the wall time includes tinymold fresh restore before and fresh record after every build, and the worktree and branch-switch cases include restoring a target/ snapshot.

ripgrep 14.1.1 (small, link-bound binary)

Case baseline sccache tinymold
Clean build 3.35s · link 0.36s · 33 compiled 1.89s · link 0.31s · 33 compiled · 64/96 hits · first (cold) run 4.09s 1.09s · link 0.00s · 33 compiled · 78/120 hits · first (cold) run 4.34s
No-op rebuild 0.06s · link 0.00s · 0 compiled 0.03s · link 0.00s · 0 compiled 0.05s · link 0.00s · 0 compiled
Comment edit 0.53s · link 0.06s · 1 compiled 0.90s · link 0.05s · 1 compiled 0.55s · link 0.07s · 1 compiled · 0/3 hits
Private function body edit 0.59s · link 0.06s · 1 compiled 0.93s · link 0.05s · 1 compiled 0.60s · link 0.06s · 1 compiled · 0/3 hits
Public signature edit 0.59s · link 0.07s · 4 compiled 1.23s · link 0.05s · 4 compiled · 0/9 hits 0.62s · link 0.07s · 4 compiled · 0/12 hits
Second worktree, same commit 3.39s · link 0.36s · 33 compiled 1.86s · link 0.31s · 33 compiled · 88/96 hits 0.08s · link 0.00s · 0 compiled
Every source file touched, no content change 0.84s · link 0.10s · 9 compiled 1.00s · link 0.09s · 9 compiled · 24/24 hits 0.04s · link 0.00s · 0 compiled
Branch switch A→B→A 1.18s · link 0.13s · 8 compiled 2.19s · link 0.11s · 8 compiled · 9/18 hits 0.43s · link 0.00s · 8 compiled · 24/24 hits
cargo test --no-run 2.82s · link 0.16s · 6 compiled 2.16s · link 0.16s · 6 compiled · 8/12 hits 0.29s · link 0.00s · 6 compiled · 16/27 hits

target/ by kind (baseline):

After rlib rmeta debuginfo objects incremental executables other total
Clean build 82 MiB 27 MiB 0 MiB 73 MiB 100 MiB 36 MiB 0 MiB 318 MiB
cargo test --no-run 95 MiB 29 MiB 0 MiB 105 MiB 156 MiB 49 MiB 0 MiB 434 MiB

tinymold ef5ac44 (this repository, as a small workspace)

Case baseline sccache tinymold
Clean build 4.22s · link 0.34s · 32 compiled 2.07s · link 0.33s · 32 compiled · 66/99 hits · first (cold) run 5.32s 1.25s · link 0.00s · 32 compiled · 78/123 hits · first (cold) run 4.21s
No-op rebuild 0.07s · link 0.00s · 0 compiled 0.04s · link 0.00s · 0 compiled 0.04s · link 0.00s · 0 compiled
Comment edit 0.17s · link 0.04s · 1 compiled 0.16s · link 0.04s · 1 compiled 0.15s · link 0.05s · 1 compiled · 0/3 hits
Private function body edit 0.34s · link 0.04s · 1 compiled 0.51s · link 0.04s · 1 compiled · 0/3 hits 0.34s · link 0.04s · 1 compiled · 0/6 hits
Public signature edit 0.61s · link 0.05s · 5 compiled 1.03s · link 0.04s · 5 compiled · 0/15 hits 0.60s · link 0.04s · 5 compiled · 0/18 hits
Second worktree, same commit 4.17s · link 0.36s · 32 compiled 2.03s · link 0.29s · 32 compiled · 92/99 hits 0.07s · link 0.00s · 0 compiled
Every source file touched, no content change 0.60s · link 0.05s · 5 compiled 0.35s · link 0.04s · 5 compiled · 15/15 hits 0.04s · link 0.00s · 0 compiled
Branch switch A→B→A 1.32s · link 0.10s · 10 compiled 0.71s · link 0.08s · 10 compiled · 30/30 hits 0.49s · link 0.00s · 10 compiled · 36/36 hits
cargo test --no-run 2.75s · link 0.40s · 9 compiled 1.50s · link 0.35s · 9 compiled · 16/24 hits 0.55s · link 0.00s · 9 compiled · 32/48 hits

target/ by kind (baseline):

After rlib rmeta debuginfo objects incremental executables other total
Clean build 54 MiB 8 MiB 0 MiB 54 MiB 32 MiB 28 MiB 0 MiB 175 MiB
cargo test --no-run 62 MiB 14 MiB 0 MiB 66 MiB 55 MiB 43 MiB 0 MiB 240 MiB

bat v0.24.0 (binary with build scripts and C dependencies)

Case baseline sccache tinymold
Clean build 7.34s · link 1.47s · 109 compiled 11.13s · link 1.41s · 109 compiled · 698/1047 hits · first (cold) run 11.69s 5.27s · link 0.09s · 109 compiled · 259/393 hits · first (cold) run 14.33s
No-op rebuild 0.10s · link 0.00s · 0 compiled 0.06s · link 0.00s · 0 compiled 0.09s · link 0.00s · 0 compiled
Comment edit 0.85s · link 0.08s · 1 compiled 0.68s · link 0.09s · 1 compiled · 3/3 hits 0.70s · link 0.09s · 1 compiled · 3/6 hits
Private function body edit 0.86s · link 0.08s · 1 compiled 0.66s · link 0.07s · 1 compiled · 3/3 hits 0.69s · link 0.09s · 1 compiled · 3/6 hits
Public signature edit 0.88s · link 0.08s · 1 compiled 1.16s · link 0.07s · 1 compiled · 0/3 hits 0.88s · link 0.07s · 1 compiled · 0/6 hits
Second worktree, same commit 7.17s · link 1.39s · 109 compiled 4.50s · link 1.28s · 109 compiled · 823/1047 hits 1.55s · link 0.08s · 1 compiled · 0/3 hits
Every source file touched, no content change 0.99s · link 0.11s · 1 compiled 0.75s · link 0.10s · 1 compiled · 3/3 hits 0.09s · link 0.00s · 0 compiled
Branch switch A→B→A 1.72s · link 0.14s · 2 compiled 1.80s · link 0.13s · 2 compiled · 3/6 hits 1.44s · link 0.17s · 2 compiled · 6/12 hits
cargo test --no-run 2.25s · link 2.66s · 29 compiled 1.78s · link 2.47s · 29 compiled · 54/81 hits 0.71s · link 0.00s · 29 compiled · 94/141 hits

target/ by kind (baseline):

After rlib rmeta debuginfo objects incremental executables other total
Clean build 199 MiB 61 MiB 0 MiB 155 MiB 46 MiB 82 MiB 9 MiB 552 MiB
cargo test --no-run 220 MiB 69 MiB 0 MiB 183 MiB 91 MiB 225 MiB 9 MiB 797 MiB

A/B: baseline vs cgu-128 (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 7 runs, 2026-10-14)

cgu-128: profile.dev.codegen-units=128. Each edit is built by both sides back to back, in alternating order, each in its own target directory. Cells: median wall time (min–max).

Project Edit baseline cgu-128 change
ripgrep 14.1.1 Comment edit 0.639s (0.63–1.05) 0.529s (0.47–0.54) -17%
ripgrep 14.1.1 Private function body edit 0.674s (0.65–1.09) 0.625s (0.56–3.71) -7%
ripgrep 14.1.1 Public signature edit 0.869s (0.81–4.55) 1.058s (0.57–3.93) +22%
bat v0.24.0 Comment edit 1.015s (0.93–1.09) 0.767s (0.72–0.85) -24%
bat v0.24.0 Private function body edit 1.013s (0.95–1.10) 0.802s (0.78–0.83) -21%
bat v0.24.0 Public signature edit 1.057s (1.02–1.13) 0.850s (0.82–0.88) -20%

A/B: baseline vs cgu-32 (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 7 runs, 2026-10-14)

cgu-32: profile.dev.codegen-units=32. Each edit is built by both sides back to back, in alternating order, each in its own target directory. Cells: median wall time (min–max).

Project Edit baseline cgu-32 change
ripgrep 14.1.1 Comment edit 0.579s (0.56–0.72) 0.451s (0.42–1.21) -22%
ripgrep 14.1.1 Private function body edit 0.620s (0.59–1.02) 0.711s (0.68–0.81) +15%
ripgrep 14.1.1 Public signature edit 0.712s (0.69–0.76) 0.487s (0.47–0.59) -32%
bat v0.24.0 Comment edit 0.878s (0.84–1.00) 0.607s (0.59–0.65) -31%
bat v0.24.0 Private function body edit 0.903s (0.86–0.95) 0.663s (0.64–0.70) -27%
bat v0.24.0 Public signature edit 0.941s (0.91–1.02) 0.678s (0.67–0.71) -28%

A/B: baseline vs cgu-64 (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 7 runs, 2026-10-15)

cgu-64: profile.dev.codegen-units=64. Each edit is built by both sides back to back, in alternating order, each in its own target directory. Cells: median wall time (min–max).

Project Edit baseline cgu-64 change
ripgrep 14.1.1 Comment edit 0.598s (0.57–0.64) 0.512s (0.44–0.69) -14%
ripgrep 14.1.1 Private function body edit 0.641s (0.61–0.66) 0.558s (0.51–0.58) -13%
ripgrep 14.1.1 Public signature edit 0.640s (0.61–0.69) 0.507s (0.49–0.56) -21%
ripgrep 14.1.1 Clean build 3.969s (3.85–9.14) 3.723s (3.55–6.02) -6%
bat v0.24.0 Comment edit 0.807s (0.77–1.07) 0.680s (0.64–1.35) -16%
bat v0.24.0 Private function body edit 0.830s (0.82–0.86) 0.688s (0.67–0.81) -17%
bat v0.24.0 Public signature edit 0.862s (0.85–1.07) 0.736s (0.68–0.81) -15%
bat v0.24.0 Clean build 7.835s (7.62–19.52) 8.006s (7.72–13.47) +2%
tinymold 3574dd6 Comment edit 0.166s (0.16–0.18) 0.172s (0.17–0.18) +4%
tinymold 3574dd6 Private function body edit 0.327s (0.31–0.35) 0.282s (0.27–0.30) -14%
tinymold 3574dd6 Public signature edit 0.631s (0.57–0.78) 0.548s (0.47–0.64) -13%

Edit-loop experiments (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 5 runs, 2026-10-14)

Each variant is layered on the baseline. Cells: median wall time, the change against the reference (the stable baseline, or baseline+nightly for nightly variants), and median link time.

Variant Settings
cgu-16 profile.dev.codegen-units=16
cgu-32 profile.dev.codegen-units=32
cgu-64 profile.dev.codegen-units=64
cranelift RUSTFLAGS -Zcodegen-backend=cranelift; toolchain nightly
deps-nodebug profile.dev.package."*".debug=false
line-tables profile.dev.debug="line-tables-only"; profile.dev.package."*".debug=false
nightly toolchain nightly
nodebug profile.dev.debug=false; profile.dev.package."*".debug=false
threads-8 RUSTFLAGS -Zthreads=8; toolchain nightly

ripgrep 14.1.1

Case baseline baseline+deps-nodebug baseline+line-tables baseline+nodebug baseline+cgu-16 baseline+nightly baseline+threads-8 baseline+cranelift baseline+cgu-32 baseline+cgu-64
No-op rebuild 0.06s · link 0.00s 0.07s (+8%) · link 0.00s 0.07s (+3%) · link 0.00s 0.06s (-1%) · link 0.00s 0.07s (+5%) · link 0.00s 0.07s (+3%) · link 0.00s 0.07s (-0%) · link 0.00s 0.07s (+3%) · link 0.00s – –
Comment edit 0.66s · link 0.06s 0.61s (-8%) · link 0.06s 0.56s (-15%) · link 0.06s 0.62s (-6%) · link 0.06s 0.45s (-32%) · link 0.06s 0.62s (-6%) · link 0.06s 0.61s (-2%) · link 0.06s 0.64s (+3%) · link 0.06s 0.48s (-28%) · link 0.06s 0.56s (-16%) · link 0.06s
Private function body edit 0.73s · link 0.06s 0.62s (-14%) · link 0.06s 0.60s (-18%) · link 0.06s 0.65s (-10%) · link 0.06s 0.88s (+20%) · link 0.05s 0.64s (-12%) · link 0.06s 0.65s (+2%) · link 0.06s 0.64s (-0%) · link 0.06s 0.80s (+10%) · link 0.06s 0.57s (-22%) · link 0.06s
Public signature edit 0.96s · link 0.06s 0.63s (-35%) · link 0.06s 0.66s (-31%) · link 0.06s 0.69s (-29%) · link 0.06s 0.47s (-51%) · link 0.05s 0.70s (-27%) · link 0.06s 0.74s (+6%) · link 0.06s 0.76s (+9%) · link 0.06s 0.58s (-39%) · link 0.06s 0.66s (-31%) · link 0.06s

bat v0.24.0

Case baseline baseline+deps-nodebug baseline+line-tables baseline+nodebug baseline+cgu-16 baseline+nightly baseline+threads-8 baseline+cgu-32 baseline+cgu-64
No-op rebuild 0.10s · link 0.00s 0.10s (+4%) · link 0.00s 0.10s (+5%) · link 0.00s 0.11s (+11%) · link 0.00s 0.09s (-5%) · link 0.00s 0.10s (+0%) · link 0.00s 0.11s (+14%) · link 0.00s – –
Comment edit 1.18s · link 0.07s 1.04s (-11%) · link 0.07s 1.06s (-10%) · link 0.07s 1.23s (+4%) · link 0.08s 0.72s (-39%) · link 0.07s 0.97s (-18%) · link 0.07s 1.06s (+9%) · link 0.08s 1.09s (-8%) · link 0.08s 1.02s (-13%) · link 0.09s
Private function body edit 1.28s · link 0.08s 1.08s (-16%) · link 0.07s 1.11s (-13%) · link 0.07s 1.06s (-17%) · link 0.07s 0.79s (-39%) · link 0.07s 1.03s (-20%) · link 0.07s 1.06s (+3%) · link 0.07s 1.18s (-8%) · link 0.09s 1.01s (-21%) · link 0.09s
Public signature edit 1.28s · link 0.08s 1.09s (-15%) · link 0.07s 1.14s (-11%) · link 0.07s 0.98s (-23%) · link 0.07s 0.81s (-37%) · link 0.07s 1.16s (-9%) · link 0.07s 1.16s (+0%) · link 0.07s 1.09s (-15%) · link 0.08s 1.02s (-20%) · link 0.08s

tinymold c79a959

Case baseline baseline+deps-nodebug baseline+line-tables baseline+nodebug baseline+cgu-16 baseline+nightly baseline+threads-8 baseline+cranelift baseline+cgu-32 baseline+cgu-64
No-op rebuild 0.06s · link 0.00s 0.06s (+2%) · link 0.00s 0.06s (+3%) · link 0.00s 0.07s (+15%) · link 0.00s 0.06s (-2%) · link 0.00s 0.07s (+3%) · link 0.00s 0.07s (+2%) · link 0.00s 0.07s (+2%) · link 0.00s – –
Comment edit 0.18s · link 0.05s 0.17s (-6%) · link 0.04s 0.17s (-2%) · link 0.04s 0.20s (+12%) · link 0.04s 0.16s (-8%) · link 0.04s 0.18s (+2%) · link 0.04s 0.20s (+9%) · link 0.04s 0.19s (+7%) · link 0.04s 0.18s (+4%) · link 0.05s 0.18s (+2%) · link 0.04s
Private function body edit 0.36s · link 0.04s 0.37s (+2%) · link 0.05s 0.36s (-1%) · link 0.04s 0.39s (+8%) · link 0.04s 0.40s (+10%) · link 0.04s 0.40s (+10%) · link 0.04s 0.42s (+5%) · link 0.04s 0.40s (+1%) · link 0.04s 0.39s (+8%) · link 0.05s 0.30s (-16%) · link 0.04s
Public signature edit 0.63s · link 0.05s 0.61s (-3%) · link 0.04s 0.65s (+3%) · link 0.04s 0.66s (+5%) · link 0.04s 0.44s (-29%) · link 0.04s 0.72s (+15%) · link 0.04s 0.80s (+11%) · link 0.04s 0.81s (+12%) · link 0.04s 0.50s (-20%) · link 0.05s 0.55s (-12%) · link 0.05s

Baseline: plain cargo, default linker (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 5 runs, 2026-10-14)

What every tinymold phase is measured against. Times are wall clock; loops report the median.

Scenario ripgrep 14.1.1 bat v0.24.0
Clean build 3.5s 21.6s
No-op rebuild 0.07s 0.10s
One-line edit → rebuilt binary 0.52s 0.75s
Every source file touched, no content change 0.84s 0.91s
Build tests (after the binary) 3.3s 4.6s
Second worktree, same commit, clean build 3.8s 13.2s
target/ after binary + tests 825 MiB 953 MiB
Second worktree deps/ artifacts 195 MiB 434 MiB
…of which byte-identical to the first worktree's 120 MiB (61%) 360 MiB (83%)

Clean builds: no cache, dry run, cached run (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 3 runs, 2026-10-14)

Cache: sccache 0.17.0, private directory and port. Each cell is the median clean build; hits and misses count rustc invocations.

Scenario ripgrep 14.1.1 bat v0.24.0
No cache 3.3s 7.5s
Empty cache (dry run, fills it) 4.2s 11.2s
Warm cache (cached run) 1.9s 6.5s
Warm cache, second worktree at another path 1.8s 8.9s
Cached run hit rate 32/32 hits 349/349 hits
Second worktree hit rate 32/32 hits 349/349 hits
Not cacheable (bins, proc-macros, incremental, …) 0 0
Cache size on disk 36 MiB 128 MiB

A fresh CI runner (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 3 runs, 2026-10-14)

cargo test --no-run --locked --package <package> on a fresh checkout at the same path, with nothing on disk but what the job restores. Each cell is the median of restore plus build; downloading an archive from GitHub's cache is not included, so archive sizes are listed for that estimate.

Mode ripgrep 14.1.1 bat v0.24.0 tinymold f3767e1
Cold, no cache 3.9s 9.9s 7.6s
target/ restored, sources touched 1.8s 3.3s 3.3s
tinymold store restored (GitHub backend) 1.9s (47/49 hits) 6.7s (177/178 hits) 3.8s (129/132 hits)
tinymold from bazel-remote only (remote backend) 1.4s (47/49 hits) 7.1s (175/178 hits) 2.9s (129/132 hits)
target/ archive 259 MiB 698 MiB 559 MiB
tinymold store archive 241 MiB 592 MiB 527 MiB

Cache hits on many-output units (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 7 runs, 2026-10-15)

Every source file touched, then rebuilt through the wrapper, so each workspace unit is a cache hit. Incremental units with profile.dev.split-debuginfo="unpacked" keep one object file per codegen unit, so a hit places hundreds of files. Before is the --against revision, after is the checkout's; they take alternate turns on the same store. Restore times come from TINYMOLD_TRACE; cells are medians (min–max). A crate's row adds up all of its units, such as a library and a binary of the same name.

Project What before after change
ripgrep 14.1.1
tinymold 185d6ba → 7312d99
Build wall time 0.26s (0.24–0.33) 0.18s (0.16–0.20) -30%
ripgrep 14.1.1
tinymold 185d6ba → 7312d99
All 10 restores, summed 315 ms (300–365) 31 ms (28–42) -90%
ripgrep 14.1.1
tinymold 185d6ba → 7312d99
rg restore (258 files) 43.6 ms (41.3–59.4) 3.4 ms (3.2–4.3) -92%
ripgrep 14.1.1
tinymold 185d6ba → 7312d99
ignore restore (212 files) 70.0 ms (64.6–86.0) 5.8 ms (4.5–6.6) -92%
ripgrep 14.1.1
tinymold 185d6ba → 7312d99
globset restore (132 files) 47.6 ms (44.5–54.5) 2.9 ms (2.6–7.2) -94%
bat v0.24.0
tinymold 185d6ba → 7312d99
Build wall time 0.60s (0.57–0.75) 0.59s (0.53–0.63) -1%
bat v0.24.0
tinymold 185d6ba → 7312d99
All 2 restores, summed 44 ms (44–55) 7 ms (7–8) -84%
bat v0.24.0
tinymold 185d6ba → 7312d99
bat restore (1581 files) 43.8 ms (43.2–54.3) 6.6 ms (6.3–7.1) -85%
bat v0.24.0
tinymold 185d6ba → 7312d99
build_script_build restore (0 files) 0.5 ms (0.5–0.7) 0.5 ms (0.5–0.5) +0%
tinymold 7312d99
tinymold 185d6ba → 7312d99
Build wall time 0.22s (0.21–0.30) 0.12s (0.10–0.14) -48%
tinymold 7312d99
tinymold 185d6ba → 7312d99
All 6 restores, summed 152 ms (149–175) 30 ms (28–50) -80%
tinymold 7312d99
tinymold 185d6ba → 7312d99
tinymold restore (211 files) 36.4 ms (34.9–38.0) 6.7 ms (6.1–8.2) -82%
tinymold 7312d99
tinymold 185d6ba → 7312d99
tinymold_remote restore (191 files) 33.1 ms (31.8–33.7) 5.4 ms (5.1–5.6) -84%
tinymold 7312d99
tinymold 185d6ba → 7312d99
tinymold_wrap restore (169 files) 28.8 ms (27.9–30.1) 5.1 ms (4.7–5.6) -82%

Running tests again (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 3/5 runs by row, 2026-10-15)

cargo test for the project's package, built through RUSTC_WRAPPER=tinymold on both sides; only the runner differs. again: nothing changed. private_body: a one-line private edit before every run. Cells are median wall time (min–max) and test binaries replayed / run by tinymold run-test; runs is how many measurements each side took.

Settings: cargo build jobs: 18; target filesystem: apfs; rustc wrapper: tinymold on both sides, one fresh store per project; cargo runner: cargo side: none, any CARGO_TARGET_RUNNER removed; run-test side: CARGO_TARGET_RUNNER="tinymold run-test"; linker: cc, rustc's default, with no timing wrapper; incremental: on (CARGO_INCREMENTAL=1); tmpdir: one per project, emptied before every measurement; doctests: run on both sides; rustdoc does not go through the runner.

Project Case runs cargo run-test replayed / ran change
ripgrep 14.1.1 again 5 1.29s (1.03–1.60) 0.09s (0.07–0.11) 2 / 0 -93%
ripgrep 14.1.1 private_body 5 5.39s (4.82–6.21) 4.68s (4.61–6.76) 0 / 2 -13%
tinymold bb944ae again 3 18.09s (11.83–18.27) 1.97s (1.53–4.74) 5 / 0 -89%
tinymold bb944ae private_body 3 15.45s (15.38–21.73) 20.69s (13.96–21.17) 0 / 5 +34%

OpenHuman openhuman-core in Docker (linux aarch64, 12 CPUs, 16 GiB, rustc 1.96.1 (31fca3adb 2026-06-26), 2026-10-15)

OpenHuman 9a0a654, dev profile, -j 12, sources and store on ext4 in a Docker volume (mold 3.0.0, GNU ld (GNU Binutils for Debian) 2.40, sccache 0.18.0; host: darwin arm64, 18 CPUs). Each cell: median wall time of 3 runs (one for a cold build), link time, units compiled, and cache hits (summed over runs). ld and mold have no cache, so their warm and second-checkout builds would be their cold build. A second checkout's repeat runs reuse what its first run stored at that path, so the first run is shown when it differs.

Case ld mold tinymold sccache sccache-noinc
Clean build, empty cache 167.2s · link 22.06s · 421 compiled 175.2s · link 3.16s · 421 compiled 176.0s · link 4.06s · 421 compiled · 0/474 hits 200.3s · link 8.59s · 421 compiled · 0/378 hits 195.9s · link 6.52s · 421 compiled · 0/430 hits
No-op rebuild 0.2s · link 0.00s · 0 compiled 0.3s · link 0.00s · 0 compiled 0.2s · link 0.00s · 0 compiled 0.3s · link 0.00s · 0 compiled 0.3s · link 0.00s · 0 compiled
Edit the binary's main.rs 15.4s · link 14.73s · 1 compiled 1.1s · link 0.61s · 1 compiled 2.1s · link 0.63s · 1 compiled · 0/3 hits 1.6s · link 0.96s · 1 compiled 1.1s · link 0.59s · 1 compiled
Edit the core library's lib.rs 32.9s · link 16.49s · 5 compiled 16.4s · link 0.67s · 5 compiled 19.1s · link 0.00s · 5 compiled · 12/15 hits 19.5s · link 1.34s · 5 compiled 138.9s · link 0.68s · 5 compiled · 6/12 hits
Clean target/, warm cache - - 4.5s · link 0.00s · 421 compiled · 1410/1422 hits 185.5s · link 3.18s · 421 compiled · 1134/1134 hits 13.4s · link 2.68s · 421 compiled · 1290/1290 hits
Second checkout at another path - - 5.8s · link 0.00s · 421 compiled · 1287/1422 hits · first run 165.1s 213.3s · link 4.46s · 421 compiled · 1065/1134 hits 13.3s · link 3.11s · 421 compiled · 1169/1290 hits · first run 187.4s
Second checkout from a target/ snapshot - - 4.3s · link 0.00s · 6 compiled · 12/18 hits · restore 6.4s · first run 155.3s - -

tinymold+mold against plain mold, by median wall time plus any snapshot restore; cached cases compare against the cold mold build: clean build, empty cache: 1.0x; no-op rebuild: 1.1x; edit the binary's main.rs: 0.5x; edit the core library's lib.rs: 0.9x; clean target/, warm cache: 38.8x; second checkout at another path: 30.2x (first run at a new path 1.1x); second checkout from a target/ snapshot: 16.3x (first run at a new path 1.1x).

sccache+mold against plain mold, by median wall time plus any snapshot restore; cached cases compare against the cold mold build: clean build, empty cache: 0.9x; no-op rebuild: 1.0x; edit the binary's main.rs: 0.7x; edit the core library's lib.rs: 0.8x; clean target/, warm cache: 0.9x; second checkout at another path: 0.8x.

sccache-noinc+mold against plain mold, by median wall time plus any snapshot restore; cached cases compare against the cold mold build: clean build, empty cache: 0.9x; no-op rebuild: 0.9x; edit the binary's main.rs: 1.0x; edit the core library's lib.rs: 0.1x; clean target/, warm cache: 13.1x; second checkout at another path: 13.2x (first run at a new path 0.9x).

Link time, GNU ld over mold: clean build, empty cache: 7.0x; edit the binary's main.rs: 24.3x; edit the core library's lib.rs: 24.8x.

Dedup probe: second worktree vs first (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 1 runs, 2026-10-14)

Share of the second worktree's target/ bytes that are byte-identical to something in the first's, at three granularities. Incremental state is excluded.

Project Files rlib members DWARF sections
tinymold 9db903e 79% of 116 MiB 92% of 43 MiB 92% of 30 MiB

Edit-rebuild loop (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 5 runs, 2026-10-14)

ripgrep edit-rebuild by linker

Project Linker Edit → rebuilt binary (median) min–max Full build Binary target/
ripgrep 14.1.1 default 0.91s 0.87–0.98s 9.3s 15 MiB 451 MiB
ripgrep 14.1.1 rust-lld 0.94s 0.89–1.12s 9.0s 14 MiB 450 MiB

bat edit-rebuild by linker

Project Linker Edit → rebuilt binary (median) min–max Full build Binary target/
bat v0.24.0 default 1.07s 0.84–5.51s 20.5s 19 MiB 693 MiB
bat v0.24.0 rust-lld 3.50s 2.57–3.72s 29.8s 18 MiB 689 MiB

Restoring a 512 MiB artifact (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 5 runs, 2026-10-14)

Method Median min–max
python byte copy (baseline) 221.0 ms 214.4–232.3 ms
tinymold cas get --strategy reflink 2.8 ms 2.4–5.8 ms
tinymold cas get --strategy hardlink 2.9 ms 2.7–3.1 ms
tinymold cas get --strategy copy 325.6 ms 311.6–443.3 ms

Cache server (darwin arm64, 18 CPUs, rustc 1.99.0 (b940084d7 2026-09-28), 5 runs, 2026-10-15)

Medians over runs; tinymold serve: release build, tinymold-4bd43ab52852, uncompressed store; bazel-remote: native, github.com/buchgr/bazel-remote/v2 v2.6.2, default zstd storage. Small hits are 2000 GETs per run; cold start opens a store of 20000 action results. Not measured: FindMissingBlobs (gRPC).

Case tinymold serve bazel-remote
hit 1 KiB p50 (ms) 0.1622 0.1357
hit 1 KiB p99 (ms) 0.3102 0.3794
download 4 MiB (MB/s) 1372.47 982.486
upload 4 MiB (MB/s) 461.219 348.421
cold start (s) 0.0171 0.0674

Edit sequence

Cumulative edits with build, clippy, tests and coverage after each, per mode and linker: see EDITS.md.

Reproduce

Requirements: a Rust toolchain, git, and Python 3.9 or newer. No Python packages are needed.

python3 bench/bench.py scenarios --runs 3     # clones the corpus into bench/.work/
python3 bench/bench.py scenarios --project ripgrep --modes baseline --cases clean,comment
python3 bench/bench.py scenarios --project ripgrep --modes baseline \
    --variants line-tables,cgu-64 --cases comment,private_body   # edit-loop experiments
python3 bench/bench.py ab --variant cgu-64 --runs 7              # interleaved A/B
python3 bench/bench.py dedup
python3 bench/bench.py baseline --runs 5
python3 bench/bench.py cache --runs 3         # needs sccache on PATH
python3 bench/bench.py link --runs 5
python3 bench/bench.py cas --runs 5 --size-mb 512
python3 bench/report.py                       # refresh the tables and charts

To see where one build's time goes rather than how long it takes (critical path, idle job slots, build scripts, links, wrapper steps), use bench/profile_build.py; see PROFILING.md. For a large real project, DOCKER.md builds OpenHuman on Linux in Docker with GNU ld, mold, tinymold, and sccache, cold, warm, and in the edit loop. Use ab for any claim about a setting. scenarios --variants runs each side to completion before the next, so load drift on a shared machine biases it; ab builds every edit with both sides back to back, in alternating order.

Every corpus build runs as stock cargo. Cargo reads config.toml from CARGO_HOME and every ancestor directory, so a developer's own tuning (a rustc-wrapper such as sccache, reduced debuginfo, a jobs limit, incremental off) would otherwise leak into the numbers. The harness disables any rustc wrapper, turns incremental on, forces full debuginfo, sets jobs to the CPU count through --config (which outranks every config file), and records those settings in each result. The cache benchmark and the sccache mode then add sccache back explicitly, on a private port and directory. sccache refuses to run while CARGO_INCREMENTAL=1 is set, so under sccache workspace crates build with cargo's default incremental setting and are reported as not cacheable.

The scenarios benchmark also pins and records the filesystem type of the build directory, the linker and its thread setting, and output reuse. Single-project runs (--project) replace that project's rows in the results file and keep the rest.

link measures every linker it finds: the default, the rust-lld bundled with the toolchain, and on Linux mold and wild when they're on PATH. Install those to include them. Each linker builds into its own directory under the corpus project's target/, so linkers never invalidate each other's artifacts. The first build per linker is a full build (reported as "Full build"); the edit loop is timed after it.

Commit the JSON files from bench/results/ along with the regenerated README.md and SVG charts. The result file name carries the OS and architecture, so results from different machines sit side by side.

Corpus

corpus.json pins each project to an exact tag:

Project Revision Role Public-signature edit
ripgrep 14.1.1 small, link-bound binary crates/searcher/src/lib.rs
bat v0.24.0 build scripts and C dependencies src/lib.rs
tinymold this repository's HEAD small workspace crates/tinymold-hash/src/lib.rs
tokio tokio-1.53.3 test-heavy library, 173 integration test files tokio/src/lib.rs
Bevy v0.19.1 deep graph, link-bound 3d_scene example crates/bevy_utils/src/lib.rs

Each project names a file and anchor for every edit kind in corpus.json. A comment edit appends a comment. A private body edit inserts std::hint::black_box(n); after a function's opening line, so the crate recompiles but its interface doesn't change. A public signature edit adds a pub fn to a library that other crates depend on, so every dependent rebuilds.

Phase 1 of the roadmap may still add a -sys-heavy workspace. Add a project by appending to corpus.json with a tag or commit, the package and what to build (bin, example, or a library with optional features), and an edits entry for each edit kind.

Methodology

  • Wall clock, median of N. Each sample is one cargo build process, timed end to end with time.perf_counter. The min and max are reported so noisy runs are visible.
  • The edit is a comment. Appending a comment changes spans, so rustc recompiles the edited crate and cargo relinks the binary. This is the smallest edit a developer makes, which isolates the fixed cost of a rebuild.
  • Everything that moves the result is recorded. OS, kernel release, architecture, CPU count, and rustc -V go into every result file. Linker benchmarks are notoriously sensitive: in a dispute between the wild and mold authors, filesystem, output-file reuse, fork mode, and thread count alone moved one ratio from 1.0x to 0.6x (research). The Phase 1 harness pins those too.
  • cas includes process start-up. Each tinymold cas get sample is a full process launch, which is what a build tool would pay per artifact today. The hot cache server in Phase 6 removes that cost.
  • No cherry-picking. Every run's samples are stored in the JSON files.

Published numbers from elsewhere

These are other people's measurements, kept here for context. tinymold has not reproduced them.

Linking debug builds on a Threadripper 7980X (64 cores) lld wild mold
Chromium 145 (4.51 GiB output) 16.64 s 3.98 s 1.65 s
Clang 21 (4.19 GiB) 6.19 s 3.71 s 1.34 s
ClickHouse 26.1 (5.58 GiB) 6.62 s 4.40 s 0.98 s
Blender 5.2 (2.46 GiB) 4.72 s 1.98 s 0.86 s

Source: the mold README, August 2026, medians of 3 runs. The Rust project reports that switching ripgrep's debug build to lld makes incremental rebuilds about 40% faster end to end (Rust blog).