17°
Portada del artículo: Wild beats mold linking Rust 20 times out of 20, and in release the linker is no longer the bottleneck
RustLinkersBenchmarksPerformance

Wild beats mold linking Rust 20 times out of 20, and in release the linker is no longer the bottleneck

I measured Wild 0.10.0, mold 2.42.1, rust-lld and GNU ld linking ripgrep and cargo. Wild won all 20 repetitions of each project, by 1.9 ms and about 8 ms. With either one the link is close to 1% of the rebuild. And on the way I almost published false numbers twice.

Efrain Garay 18 September 2026

Playing summary

On 18 September David Lattimore, the author of Wild, published his own benchmark of Wild against mold. That same day I had both linkers downloaded and two real Rust projects to link: ripgrep and cargo itself. I wanted to know which one suits my machine, and how much I am losing by staying with the linker that ships with rustc.

Wild was faster than mold in all 20 repetitions of each project. The lead is 1.9 ms on ripgrep and about 8 ms on cargo, in a cycle where the rest of the rebuild, without the link, takes 2.2 to 2.6 s. With either of them, the link is close to 1% of the rebuild. And before getting to those numbers I came close to publishing other, false ones. Twice.

In 69 seconds and narrated: Wild against mold linking ripgrep and cargo, median of 20 repetitions. Wild won all 20 of each project, by 1.9 ms on ripgrep and about 8 ms on cargo. With rust-lld as the default linker, linking is 2 to 5% of a release rebuild of 2.2 to 2.6 s, and close to 1% with mold or Wild. It includes a mistake in my benchmark: a fake linker passed with -B never ran. Muted by default: turn the sound on in the controls.Watch it in the reel viewer →

What I measured

A linker takes the object files the compiler produces and joins them into an executable. In Rust that step runs at the end of every build of the binary, and since dependencies are linked statically, the file it writes is large: the two in this post weigh 30 and 39 MB. mold is the linker by the original author of LLD. Wild aims to be very fast for iterative development.

Four linkers over the same code:

  • GNU ld (bfd) 2.45.1, the historical one.
  • rust-lld, which is LLD 22.1.8, the one in rustc’s sysroot. It is the default linker of rustc 1.98 on the x86_64-unknown-linux-gnu target, which is mine. I checked: with no flags at all, the binary comes out signed Linker: LLD 22.1.8. The Rust blog announced the change for that target; I did not look at others, such as musl.
  • mold 2.42.1.
  • Wild 0.10.0.

Two projects. ripgrep 15.2.0 at commit 3fce3b5, with upstream’s release profile, which carries debug = 1: a 30 MB binary with DWARF. And cargo at commit 8814ead, release without DWARF, 39 MB. Do not read them as the same case in two sizes: one carries debug information and the other does not.

The machine: Fedora 43 with kernel 7.2.4, an AMD Ryzen 7 7800X3D with 8 cores and 16 threads, NVMe with ext4. rustc and cargo 1.98.0, clang 21.1.8. The bench runs with 8 physical cores visible and a memory cap:

systemd-run --user --scope -p MemoryMax=12G taskset -c 0-7 ./bench3.sh

I checked that nproc said 8 in there. AllowedCPUs is useless in my session: systemd accepts it and ignores it, because the cpuset controller is not delegated to the user. The governor stayed on powersave (amd-pstate-epp, balance_performance). I could not change it without sudo.

Installation, and the first trap

Installing was the easy part. mold and Wild publish binaries, and installing each one took under 2 s. The hard part was getting rustc to actually use them.

I tried three ways of asking for another linker. To find out which one was in charge I used a trick that has saved me before: a fake ld whose only job is to exit with code 7. If the build passes with that linker in place, nobody called it.

The sabotage: a fake linker that exits with 7, three ways inIf the build survives a linker that always fails, that linker was never called. Pick a tab; each one replays in a loop.
RUSTFLAGS-C link-arg=-fuse-ld=<absolute path to the fake ld>
ccerror: unrecognized command-line option '-fuse-ld=/…/shim/falso/ld'
It fails, and it says so. Annoying, but honest.
RUSTFLAGS-C link-arg=-B<dir holding a fake ld that exits 7>
rustc → cc-B<sysroot>/…/gcc-ld -fuse-ld=lld -B<my dir>
cc looks forld.lld: rustc’s dir is searched first (a fake ld.lld in mine: same result)
cargoFinished `release` profile [optimized + debuginfo] · exit=0
.commentLinker: LLD 22.1.8
The build passes and the fake never ran, with ld or with ld.lld. Whatever I had timed here was rust-lld.
RUSTFLAGS-Clinker=clang -Clink-arg=--ld-path=shim/crono-sabotaje/ld
clangshim → stopwatch → fake ld
stderrENLAZADOR FALSO INVOCADO
logexit code 7
cargoerror: linking with `clang` failed: exit status: 7
The build breaks. This path does run what I hand it, so this is the one I measured with.

Lines taken from sabotaje_B_fuse.txt and sabotaje3.txt, except the “clang” row, which is my reading. The fake ld.lld test and the search order are sections 4 and 5 of sabotaje_B_fuse.txt.

-C link-arg=-fuse-ld=/absolute/path fails right away: cc answers “unrecognized command-line option”. Annoying, but it tells you.

-C link-arg=-B<dir> is the dangerous one. The build passes and the alternative linker never runs. I tried it two ways. With a fake ld inside that directory the build finished fine and the binary came out signed LLD 22.1.8. That could have been the name: with -fuse-ld=lld, cc looks for a program called ld.lld, not ld. So I put a fake named ld.lld in the directory. Same result: exit 0, and the fake never ran.

cc does honour -B. Asked with cc -print-prog-name=ld.lld, it finds the fake if my directory is alone or comes first. What sinks it is the order: rustc hands cc its own -B<sysroot>/.../gcc-ld before anything you add, and the first directory holding an ld.lld wins. Mine comes second.

The Wild README lists -B <dir> among its generally supported options, and cc honours it. Added through RUSTFLAGS on rustc 1.98 with the default driver, it comes second.

Had I measured that way, every series would have been rust-lld wearing a different label.

What does decide is -Clinker=clang -Clink-arg=--ld-path=<path>, which is what the Wild README documents. You have to install clang, 65 MB. With that form, the same fake ld breaks the build: “linking with clang failed: exit status: 7”.

There is a fourth form I did not test: -fuse-ld=mold by name. It is what mold documents for GCC 12.1.0 or later and Wild for GCC 16.1 or later, and it needs the linker on the PATH. If you use it, the same fake ld tells you right away whether it is in charge.

Who picks the linker: the lane I measured and the two that liedAll three start at rustc. With clang and --ld-path my shim runs. With the default driver, rustc’s own -B comes first and mine loses. With clang and no --ld-path, GNU ld links.
Who picks the linker: the lane I measured and the two that liedmeasured: --ld-path decidestrap: my -B comes secondalmost published as LLD: clang without --ld-pathrustc 1.98.0cargo build --releaseone line into main.rsclang 21.1.8--ld-path=shim/ldshim + stopwatchEPOCHREALTIME, µslogs exit code, -oreal linkerGNU ld · rust-lldmold · Wildbinaryreadelf -p .commentcc (default)-B<sysroot>/gcc-ld-fuse-ld=lldld.lld from sysrootrust-lld · LLD 22.1.8build passesfake ld, exits 7never executedclang 21.1.8-Clinker=clangno --ld-path/usr/bin/ldGNU ld (bfd) 2.45.1my CSV said lldbinaryno LLD signaturemy -B<dir>Who picks the linker: the lane I measured and the two that liedrustc 1.98.0cargo build --releaseone line into main.rsmeasured-B, secondclang 21.1.8--ld-path=shim/ldshim + stopwatchEPOCHREALTIME, µslogs exit code, -oreal linkerGNU ld · rust-lldmold · Wildbinaryreadelf -p .commentcc (default)-B<sysroot>/gcc-ld-fuse-ld=lldld.lld = rust-lldLLD 22.1.8build passesfake ld, exits 7never executedmy -B<dir>almost published as LLDclang 21.1.8-Clinker=clangno --ld-path/usr/bin/ldGNU ld 2.45.1no LLD signature

The only lane where the alternative linker demonstrably ran is the first one: a fake linker exiting with 7 breaks the build there, and passes unnoticed in the second. In the third the build passes too, and only the missing signature gives it away.

The first bench carried two more mistakes

With --ld-path sorted out I put together a first bench. It carried two mistakes, and both produced believable numbers.

The baseline was mislabelled. For the reference series I used -Clinker=clang without --ld-path, convinced that meant LLD. With clang as the driver and no --ld-path, clang uses /usr/bin/ld, which is GNU ld. rustc only adds its -fuse-ld=lld when the driver is the default cc; ask for -Clinker=clang and it stops doing so. clang -print-prog-name=ld shows it. My CSV said “lld”, and the headline coming out of it, mold beating LLD by more than a second, was false. I found out while checking the binaries with readelf: the one from that series had no LLD signature.

And the clock was measuring something else. It timed the whole cargo build. That is an incremental rebuild: rustc recompiling the crate and, at the end, the link. The percentages coming out of it were percentages of the rebuild. That bench also said Wild and mold were tied. It was noise: a clock that counts hundredths of a second cannot separate two links that are 2 ms apart.

It is not the first time. When I measured Go’s SIMD against NumPy I also came close to publishing two false comparisons.

That first bench was not a complete waste. Its absolute differences work as cross-validation, because they come from a different timing method. It is not an independent replication: same machine, same projects. Timing the whole cargo build, GNU ld minus mold gave 0.34 s on ripgrep and 1.31 s on cargo. The final bench, timing only the linker, gives 0.33 s and 1.3 s. On the unrounded medians, they agree within 2%.

Between that bench and the final one there was an intermediate one. It already timed only the linker, but with a coarser clock and no CPU limit. I only quote one caveat from it, further down.

How the final bench ended up

The linker is picked through RUSTFLAGS:

RUSTFLAGS="-Clinker=clang -Clink-arg=--ld-path=$SHIM" cargo build --release

$SHIM is a minimal script, one per linker. It runs a stopwatch around the real linker and logs the argument of -o, so I know which file was being linked. This is the rust-lld one, with the sysroot path shortened:

#!/usr/bin/env bash
# shim/crono-lld/ld: clang runs this instead of a linker
exec "$CRONO_BIN" <sysroot>/lib/rustlib/x86_64-unknown-linux-gnu/bin/rust-lld -flavor gnu "$@"

rust-lld needs -flavor gnu to behave like ld. The GNU ld shim points at /usr/bin/ld.bfd, and the mold and Wild ones at their binaries. This is the stopwatch:

REAL="$1"; shift
out=NA; prev=
for a in "$@"; do
  [ "$prev" = "-o" ] && out=$a
  prev=$a
done
# EPOCHREALTIME uses the locale's decimal separator (a comma under es_CL).
INI=${EPOCHREALTIME/[.,]/}
"$REAL" "$@"
COD=$?
FIN=${EPOCHREALTIME/[.,]/}
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$CRONO_TAG" "$INI" "$((FIN - INI))" "$COD" "$REAL" "$out" >> "$CRONO_LOG"
exit $COD

The clock is bash’s EPOCHREALTIME: microseconds, and no fork in between. All four series go through clang and the shim, the rust-lld one included. I did not time rustc’s default path, with cc and its own ld.lld.

For each measurement I append a line to main.rs (crates/core/main.rs in ripgrep, src/bin/cargo/main.rs in cargo) and rebuild in release, the same way I measured Bun’s figures by swapping only the binary: between one run and the next, the only thing that changes is who links. Every measured build had exactly one linker invocation, the one for the executable (deps/rg-<hash>, deps/cargo-<hash>). That is 160 builds, and no build_script slipped into the count.

There are 20 repetitions per series and project, with the series interleaved and the order rotating on every repetition. All 160 finished with exit code 0. The identity of each link comes from the binary’s signature, except for GNU ld, which does not sign: there it rests on the invocation log. All 8 binaries run (rg --version, cargo --version). And the sabotage was repeated behind the new stopwatch, to check that the shim does not swallow a failure either. The confidence intervals below are percentile bootstrap over the 20 repetitions.

The numbers

Medians of 20 repetitions, to three significant figures:

GNU ldrust-lldmoldWild
ripgrep352 ms42.6 ms18.3 ms16.3 ms
cargo1362 ms121 ms35.7 ms27.9 ms
The four linkers, each lane at the time I measuredMedian of 20 repetitions of the executable's link. The tabs without GNU ld change the scale so the fight at the bottom becomes visible.
GNU ld352 ms
rust-lld42.6 ms
mold18.3 ms
Wild16.3 ms

Ten times slower than measured. 30 MB binary with DWARF.

rust-lld42.6 ms
mold18.3 ms
Wild16.3 ms

A hundred times slower than measured.

GNU ld1362 ms
rust-lld121 ms
mold35.7 ms
Wild27.9 ms

Three times slower than measured. 39 MB binary without DWARF.

rust-lld121 ms
mold35.7 ms
Wild27.9 ms

Forty times slower than measured.

Wall time until the linker returns control. mold and Wild finish their cleanup in a child process this clock does not see.

How to read this

Wild against mold. Wild was faster in all 20 repetitions of each project. On ripgrep it is 1.9 ms less (95% CI: 1.7–2.3 ms), and mold takes 1.12× what Wild takes (CI 1.10–1.14). On cargo it is about 8 ms less and 1.28× (CI 1.27–1.31), with distributions that do not overlap. The lead is real and consistent. It is also tiny.

rust-lld against both. rust-lld takes 2.3× what mold takes on ripgrep and 3.4× on cargo. Against Wild, 2.6× and 4.3–4.4×. It sounds like a lot until you turn it into milliseconds: replacing rust-lld with mold or Wild saves 24–26 ms on ripgrep and 85–93 ms on cargo.

GNU ld. It takes 8.3× and 11.2× what rust-lld takes. Here the difference does show, but it is a historical reference. In my setup it turns up when you force -Clinker=clang without --ld-path, which was exactly my mistake. It is also still in play wherever rust-lld is not the default, or if you turn it off with -C linker-features=-lld: there rustc uses the system linker.

The gap between mold and Wild is wider on cargo than on ripgrep. I do not know why, and with one project per regime I have no way to attribute it: size, DWARF and code all change at once.

For anyone building on x86_64-unknown-linux-gnu with Rust 1.90 or newer and the default linker configuration, the baseline that matters is rust-lld, because it is what they already have without touching anything. Against that baseline the question moves from “which one is faster” to “how much of the cycle is link”.

After touching main.rs in release, between 2.2 s (ripgrep) and 2.6 s (cargo) go by between the end of one link and the start of the next: medians of 2.18 s and 2.55 s over 79 gaps each. That is rustc recompiling the crate, along with cargo starting up and the bookkeeping of my bench, which I did not separate. The link comes after that.

How much of the rebuild is linkEach full bar is one release rebuild after touching main.rs. The coloured strip, to scale, is what the linker takes.

Full bar: the rest of the rebuild, without the link, 2.2–2.6 s

  1. Link with rust-lld, the default2–5%
  2. Link with mold or with Wildclose to 1%
  3. Wild's lead over mold, on ripgrep0.09%
  4. Wild's lead over mold, on cargo0.3%The last two strips are barely visible, and they are drawn at a minimum of one pixel. That is the finding.

Proportions from the bench of 18 September 2026, release profile. I measured nothing in the dev profile.

In a rebuild that takes more than two seconds, 2 ms is negligible.

The big jump already happened, and rustc made it by shipping rust-lld as the default: on cargo, from 1362 ms with GNU ld to 121 ms. What is left to gain by switching linkers is tens of milliseconds on a cycle that still lasts more than two seconds outside the link.

What these numbers do not say

  • I measured wall time until the linker returns control. mold and Wild fork by default (--fork) and finish their cleanup in a child process my clock does not see. rust-lld and GNU ld do not do that. It is the time the user waits. The total work is larger, and I did not measure by how much.
  • It is one machine, one 7-minute session, two projects and the release profile. None of this carries over to the dev profile, which I did not measure.
  • The gap between mold and Wild depends on the conditions. The intermediate bench, with no CPU limit and conditions I did not record, gave 1.31× and 1.52× instead of 1.12× and 1.28×. My suspicion is the 16 visible threads against 8 cores, but I did not check it. That is why the table and every ratio in this post come from a single bench, the one with its environment written down.
  • The 2.2–2.6 s outside the link are the gap between two consecutive links. They are not rustc on its own: they include cargo starting up and what my script does between one build and the next, and I did not measure rustc separately.
  • The stopwatch adds a fixed cost of about 0.9–1.0 ms per link. I measured it 4 times with a linker that does nothing. That squashes the ratios towards 1: the ones I publish are conservative.
  • I say nothing about memory. The first bench recorded a memory peak, but that peak was rustc’s.
  • rust-lld on ripgrep shows two modes, near 38.5 and 42.7 ms. I did not investigate the cause.
  • The rotation of series is cyclic: in 15 of the 20 repetitions each series has the same predecessor. I detect no position effect (Kruskal-Wallis, p ≥ 0.06), but with 5 measurements per position I can only rule out large effects.

It depends on the configuration, and Wild’s own author says so

What follows I read in Lattimore’s post. I did not run those projects. None of them is a Rust project, his machine is a different one, and he measured his own builds of both linkers as of 28 August 2026, which are not necessarily the published versions I measured.

His conclusion is that the result depends on the configuration. With a fresh output file on ext4 and --no-fork, Wild takes 2.23 s against 1.79 s for mold on blender-debug, and 1.11 s against 0.89 s on godot-debug: mold wins by 1.2×. On clang-release they go from a tie (Wild 0.21 s, mold 0.20 s) to Wild taking 0.6× what mold takes (Wild 0.11 s, mold 0.19 s) when linking on tmpfs, over an output that already exists and with fork. He also says the Wild build he benchmarked lacks fallocate and hugepages to create files quickly outside tmpfs, and that both are already done and coming in the next release.

My case is ext4, fork by default in both linkers and Rust binaries of 30–39 MB. It is a single point on that map. Wild winning at my point and mold winning at his debug points is no contradiction: those are projects of a different size, with a different way of writing the output.

My take

The good:

  • Wild 0.10.0 beats mold 2.42.1 in everything I measured, with intervals that do not touch 1.
  • Installing either one took under 2 s, and both sign the binary. That lets you verify instead of trusting.
  • rust-lld as the default already delivers almost all of the improvement with nothing to configure.

The bad:

  • Picking the linker from Rust has a form that fails silently. -B lets the build pass and links with something else, because rustc puts its own -B first. Without the sabotage I would not have known.
  • The only form I verified, --ld-path, means installing clang and changing the link driver for the whole project.
  • The gain over rust-lld, on my two projects and in release, is going from a link that takes 2–5% of the cycle to one that takes close to 1%.

Bottom line: if your rustc already links with rust-lld, in release and with projects like these two, the time you have left to win back is outside the linker.

When I would switch linkers and when I would not

  • I would switch if my binaries were still coming out of GNU ld. One line checks it, readelf -p .comment: if no linker signature shows up, it is worth a look.
  • I would switch if the link dominated my cycle. That is not my case with these two projects, and I measured none where it is.
  • I would try Wild before mold on a machine like mine, with ext4 and fork by default. With large debug binaries I would look at Lattimore’s table first, and I would repeat the measurement once the Wild release with fallocate is out.
  • I would not switch on rustc 1.98 on x86_64 Linux with a rebuild that spends more than two seconds outside the link. There I stay with rust-lld: zero configuration, and the difference is tens of milliseconds.
  • And either way, before believing a linker benchmark, I would hand it a fake linker that exits with 7 and look at the signature in the binary. The first kept me from measuring four series that would all have been rust-lld, and the second from a false headline. Neither of them warned me that the clock was timing the whole rebuild.

Sources

Comments

No comments yet. The first one is yours.

Reviewed before publishing. The email is not stored and never appears anywhere.