
I measured Wild against mold again and mold won: the culprit was not the new versions, it was btrfs
In September Wild beat mold linking ripgrep. I reran the bench with Wild 0.11 and mold 3.0 and mold won, also with the old binaries. In between, the machine moved from ext4 to btrfs. With target on ext4 or tmpfs Wild wins again; on btrfs Wild chooses to write from a buffer, and forcing mmap cuts 18 to 37 % of its time. In debug the link really matters: rust-lld took 37 s on btrfs and 1.9 s on ext4 for the same bevy link.
On September 18 I published that Wild beat mold linking Rust: 16.3 vs 18.3 ms on ripgrep, in all 20 runs. That post left things unmeasured: the debug profile, memory, the thread count, another machine. And in October Wild 0.11.0 and mold 3.0.0 came out, the latter being mold rewritten in Rust. So I wrote a sheet with five hypotheses and their decision rule, committed it, and ran the same bench again.
mold won. It beat Wild in 18 of 20 runs, 22.5 vs 35.3 ms, and it did so with September’s very binaries as well. The versions explained nothing. What had changed was the disk: between the two posts the machine moved from ext4 to btrfs. With target on ext4 or tmpfs Wild wins again, and on btrfs Wild picks a write mode that, in my bench, makes it slower. In debug the link stops being a detail: with bevy, rust-lld took 37 s on btrfs and 1.9 s on ext4 replaying the same link.
What changed between September and now
The machine is the same: a Ryzen 7 7800X3D with 8 cores and 16 threads and an NVMe drive. The system is not. In September it was Fedora 43 on ext4. Since October it runs NixOS, and the directory where the projects live, /data, is a btrfs subvolume mounted with compress=zstd:1. The kernel went from 7.2.4 to 6.18.55 and rustc from 1.98.0 to 1.99.1.
That is why the sheet does not compare September’s figures with today’s. The old versions of both linkers, Wild 0.10.0 and mold 2.42.1, are measured again on the new system, next to the new ones, in the same bench. The bench is September’s, unchanged: a shim with a microsecond stopwatch around the linker, one line appended to main.rs per measurement, 20 runs per arm, arms interleaved with a rotating order, 8 visible cores and a 12 GB cap.
The sheet’s five hypotheses: that Wild 0.11 is not slower than 0.10 (its notes warn that COMDAT support made it “a bit slower”), that the link weighs more in debug, that the gap between mold and Wild changes from 8 to 16 CPUs, that memory ranks the linkers differently from time, and that on a laptop the advantage shrinks. The filesystem was not on the list. It showed up in the first experiment, and what came after is in four amendments, each one committed before its measurement.
The new versions do not explain the change
Medians of 20 runs of the ripgrep and cargo link in release, with target on btrfs:
| ms | GNU ld | rust-lld | mold 2.42.1 | mold 3.0.0 | Wild 0.10.0 | Wild 0.11.0 |
|---|---|---|---|---|---|---|
| ripgrep | 401 | 45.3 | 22.5 | 22.2 | 35.3 | 34.9 |
| cargo | 1530 | 108 | 41.8 | 62.7 | 54.1 | 54.8 |
With September’s binaries, mold 2.42.1 takes 0.64 times Wild 0.10.0’s time on ripgrep (95 % CI: 0.63 to 0.65). In September it was the other way around.
Wild 0.11 vs 0.10: ratio 1.01 on ripgrep (CI 1.00 to 1.03) and 0.99 on cargo (CI 0.97 to 1.00). On cargo the interval barely leaves out 1, with 0.11 being 1.3 % slower, far below the 5 % the sheet set as the minimum effect. No relevant difference.
mold 3.0 vs 2.42.1: a tie on ripgrep (ratio 1.01, CI 1.00 to 1.04). On cargo, 3.0.0 took 62.7 ms against 41.8 ms. I do not count it as a regression: the same mold 3.0.0 arm on cargo release gave 55.3 ms and 49.4 ms in two other experiments that day. It is a single-experiment figure, and the mold 3.0 release notes say performance stayed on par with 2.42.1.
No version moves the needle. Wild kept losing with the same binaries it had won with.
The filesystem flips the order
Amendment 1 was born from that result, and it says so: it is a post hoc hypothesis. The experiment changes one thing only, where target lives. Four places on the same NVMe: btrfs with zstd (as it is), btrfs with compression turned off for that directory, tmpfs in RAM, and ext4 on a loop image. ripgrep release, 20 runs per arm and filesystem, in two rounds with the order of filesystems reversed in the second one.
Animation twenty times slower than measured. This is how /data is set up on fedora.
Animation twenty times slower than measured. Turning zstd off changes nothing.
Animation twenty times slower than measured. In RAM, Wild is back in front.
Animation twenty times slower than measured. As in September.
Fedora machine running NixOS, kernel 6.18.55, rustc 1.99.1. All four locations are on the same NVMe; ext4 is a loop image.
The amendment’s rule required the mold/Wild ratio to fall below 1 on btrfs and above 1 on ext4 or tmpfs, with intervals not touching 1. It held with room to spare: 0.63 on btrfs with zstd, 0.65 without compression, 1.20 on ext4 and 1.30 on tmpfs. And since uncompressed btrfs looks like btrfs with zstd, not like ext4, compression explains nothing.
Wild goes from 14.6 ms on ext4 to 35.1 ms on btrfs: 2.4 times. mold goes from 17.5 to 22.1 ms: 1.26 times. rust-lld, from 34.3 to 44.3 ms. All three lose on btrfs; Wild loses much more.
I checked it a second way. Wild can save the inputs of a link with WILD_SAVE_BASE and replay it later with any linker (run-with), which is the method in its own BENCHMARKING.md. Replaying the same ripgrep link 5 times per arm, with the copy on btrfs and on ext4: Wild 39.1 vs 18.5 ms, mold 25.8 vs 21.7, rust-lld 43.8 vs 40.6. Same order.
btrfsext4
The laptop confirms the ext4 side on another machine. On lenovo, a 4-core i5-8250U running NixOS with ext4 on a SATA SSD, Wild won everything I measured: ripgrep release 33.5 vs 59.7 ms for mold, ripgrep debug 72.0 vs 123.3, cargo release 65.7 vs 143.3 and cargo debug 508 vs 718 ms. The sheet’s hypothesis, that the advantage would shrink on a laptop, came out the other way: it grew.
Why btrfs hits Wild harder
perf stat over the same replayed link (mean of 5 runs, counting child processes) shows a difference that goes along with the time: on btrfs all three linkers take more page faults. Wild 16,329 vs 8,063 on ext4, mold 12,763 vs 5,388, rust-lld 18,548 vs 14,213. And strace -c shows Wild’s threads spending 0.51 s in futex on btrfs against 0.19 s on ext4. That is association: it says what changes along with the time, not what causes it.
The concrete lead came from Wild’s code. Issue #1499, “BTRFS write performance”, says that writing the output on btrfs performs badly with several threads. The answer is in libwild/src/fs.rs: if the output goes to btrfs and the kernel is older than 7.2, Wild does not write with mmap. It builds the whole file in memory and writes it at the end, and it turns huge pages off. PR #2509, which landed in 0.11.0, added the kernel exception: with 7.2 or newer it goes back to mmap. My kernel is 6.18. strace already showed it: on btrfs, Wild makes 3 write calls adding up to 21.7 ms; on ext4, write does not even show up among its main calls.
Ratios are Wild’s forced-mmap time over its default time on btrfs, median of 20 replays of the same link. With kernel 7.2 or newer the code sends btrfs down the mmap lane too; I did not measure that.
Wild has options to pick the mode by hand: --mmap-output-file and --no-mmap-output-file. Amendment 3 used them as a positive control, with a rule written before measuring: if forcing the buffer mode on ext4 made Wild at least 30 % slower, that mode reproduced the penalty; and on btrfs, the default mode and the buffer mode had to land within ±10 % of each other, to confirm it was the one Wild chose.
Animation twenty-five times slower than measured. It ran with another bench active on the machine.
Animation twenty-five times slower than measured. btrfs and ext4 on two loop images on the same SSD.
Wild 0.11.0, kernel 6.18.55. By default on btrfs with this kernel, Wild writes from a buffer; on ext4 it uses mmap.
On fedora, the forced buffer mode on btrfs came in at 0.96 times the default (CI 0.93 to 0.99): it is the same mode, as the code says. On ext4, forcing the buffer made Wild 1.24 times slower (CI 1.15 to 1.28): a real penalty, but under the 30 % the rule asked for.
That experiment has a problem, and I declared it in the sheet before looking further: it ran while week 2 of another bench was keeping the machine busy. mold, which took 22 to 26 ms in the unloaded replay, gave 68 to 71 ms here on both filesystems. The ratios between Wild’s modes are comparable, because the arms are interleaved, but the absolute figures are not. Amendment 4 repeated it on lenovo, without load, with btrfs and ext4 on two loop images on the same SSD so that only the filesystem changed.
On lenovo the mode confirmation holds again: forced buffer and default are equal on btrfs (1.00, CI 0.98 to 1.01). The positive control does not: on ext4, forcing the buffer cost only 1.04 times (CI 1.01 to 1.06). On both machines one condition out of two held, so what I can say is that the write mode is consistent with the penalty, not that it explains it. Writing from a buffer is not expensive in itself; it is expensive on btrfs.
What I did not expect: forcing mmap on btrfs made Wild faster on both machines. On fedora it came down to 0.63 times its default time (CI 0.61 to 0.67), and to 0.62 with huge pages. On lenovo, 0.82 (CI 0.82 to 0.84), and there, with mmap, Wild on btrfs ended up almost level with ext4: 43.8 vs 42.0 ms. On fedora it fell short: 32.1 ms on btrfs with mmap against 24.2 on ext4. The mode Wild picks to protect itself from btrfs costs it, in this bench and with the 6.18 kernel.
And a nuance that corrects the headline for the laptop: on lenovo, mold lost nothing on btrfs (79.0 vs 78.9 ms on ext4), and Wild beat it on both filesystems, 1.49 times on btrfs. btrfs flipping the order between the two linkers happened on fedora, with 8 fast cores and an NVMe drive, not on the laptop.
In debug, the link does matter
In September the conclusion was that in release, with rust-lld as the default, the link was no longer the bottleneck: 2 to 5 % of the rebuild. In debug the story changes, because the linker moves all the debug info. Share of the link in the rebuild (median link over median rebuild, target on btrfs):
| link share | GNU ld | rust-lld | mold 3.0.0 | Wild 0.11.0 |
|---|---|---|---|---|
| cargo | 83 % | 26 % | 16 % | 26 % |
| rust-analyzer | 94 % | 49 % | 38 % | 44 % |
| bevy (breakout) | 87 % | 96 % | 54 % | 49 % |
In release, with ripgrep and cargo, the same share was 1 to 4 % except with GNU ld.
Full bar: the whole rebuild, link included
- rust-lld, the default96 %
- GNU ld87 %
- mold 3.0.054 %
- Wild 0.11.049 %
Medians of 20 rebuilds per linker, October 2026. The debug binary weighs about 2 GB.
The extreme case is rust-lld with bevy: a median of 108.7 s inside the build, against 5.2 s for mold and 6.9 s for Wild. I treated it as a bug until I could reproduce it. Replaying the saved link, rust-lld took 37.4 s with the copy on btrfs and 1.9 s on ext4. On ext4, Wild was the fastest: 0.91 s, against 1.13 for mold. On btrfs, mold 1.42 s and Wild 2.16 s.
I cannot fully explain the gap between the 108.7 s in the build and the 37.4 s in the btrfs replay. Inside the build, the old 2 GB output exists and gets replaced, and the 5.5 GB of inputs are not always cached; the replay deletes the output first and reads warm inputs. Those are two different conditions, and I did not isolate which one weighs more.
If you build large projects in debug on btrfs with the default linker, check how long the link takes. For me it was almost two minutes of every rebuild.
Threads: parallelism explains a good part
With a single thread (--threads=1), mold takes 72.8 ms on ripgrep and Wild 85.1; on cargo, 159.3 vs 171.5. With 8 threads both drop by 2 to 3 times, and each one’s edge over the other depends largely on how it splits the work across threads. With only one, mold comes out ahead on both projects.
September’s suspicion was that the gap changed with 16 visible CPUs instead of 8. On ripgrep, mold went from 35.9 to 26.3 ms and Wild barely moved (34.6 to 33.9). On cargo both went down evenly, from 55 to 49 ms. There is a problem with that experiment: mold on ripgrep gave 22 ms in the other two btrfs experiments and 36 ms here with 8 CPUs, with no effect from its position in the rotation or from the previous arm. It is a drift between experiments I cannot explain, which is why every comparison in this post is within a single experiment.
Memory: one measurement that failed and one that works
The sheet planned to measure peak memory with /usr/bin/time -f %M. For GNU ld and rust-lld it works. For mold and Wild it gave between 5 and 11 MiB on every project, which is impossible: both fork, and the work is done by a child process that time does not measure. I publish it as a failed measurement.
Amendment 2 replaced it with the peak memory of the whole cgroup, which does include the children, replaying each saved link 3 times. Careful: that peak also counts the page cache of the output file, so it is not anonymous memory.
| cgroup peak, MiB | GNU ld | rust-lld | mold 3.0.0 | Wild 0.11.0 |
|---|---|---|---|---|
| rust-analyzer debug | 1946 | 877 | 925 | 1126 |
| bevy debug | 6246 | 3379 | 3482 | 5018 |
| cargo release | 345 | 142 | 201 | 123 |
| ripgrep release | 115 | 63 | 118 | 86 |
In debug, Wild uses more memory than mold and rust-lld: on bevy, 5 GB against 3.4 to 3.5 GB. In release the order depends on the project: Wild is the lowest on cargo and rust-lld on ripgrep. These passes ran on btrfs, where Wild builds the whole output in memory before writing it. That is consistent with its peak in debug, but I did not measure memory on ext4, so I cannot separate the two.
Controls
- The 6 linked ripgrep binaries run and produce the same search, with identical output across arms, and each binary carries its linker’s signature in
.comment(GNU ld does not sign). - The sabotaged arm, with a linker that exits with an error, failed in its 3 rows.
- I discarded 160 of 240 rows from one debug pass: the rust-analyzer and bevy filter looked for a file with a hyphen, and the bevy example had moved folders. I repeated them clean in another pass.
- No row of the original sheet’s timed experiments ran with the other bench active. E8 on fedora did, and it is marked row by row.
What these numbers do not say
- The filesystem is not the only difference from September. The kernel, the distribution and rustc changed too. What E6 isolates is the filesystem within the new system.
- Wild’s code switches mode on btrfs depending on the kernel. With a 7.2 or newer kernel, Wild uses
mmapon btrfs too, and I did not measure that: my kernel is 6.18. - The ext4 image is a loop file on the same NVMe, not a partition. In the replay, the btrfs copy lives in a compressed subvolume.
- Page faults and
futexgo along with the time. I did not prove they cause it. - E8 on fedora ran under load from another bench; its replication on lenovo, without load, uses loop images on a SATA SSD and a 4-core CPU. The two agree on the direction, not on the size.
- Absolute figures drift between experiments on btrfs (mold on ripgrep: 22 ms in one, 36 ms in another). Ratios within one experiment are what I publish.
- Time is wall time until the linker returns control. mold and Wild finish their cleanup in a child process this clock does not see, as in September.
- One desktop, one laptop, four projects.
My take
The good:
- The advantage of Wild over mold I measured in September is still there with target on ext4 or tmpfs, and on the laptop it is 1.4 to 2.2 times.
- Wild documents its btrfs problem in the code and lets you pick the write mode with a flag. That let me test it instead of guessing.
- In debug, either of the two is far ahead of rust-lld with large binaries.
The not so good:
- The result of a linker bench depends on where
targetlives, and almost nobody writes it down. I did not in September: I wrote “NVMe with ext4” and nothing else. - In my bench, the mode Wild picks for btrfs with kernel 6.18 takes 1.6 times what it takes with
mmap. - rust-lld with a 2 GB binary on btrfs reached almost two minutes per rebuild, and nothing warns you.
Bottom line: before choosing a linker from a benchmark, check which filesystem your target lives on. On ext4 and tmpfs Wild won. On btrfs, on my desktop, mold won on ripgrep, and on cargo the result changed from one experiment to another; on the laptop Wild kept winning.
When I would pick each one
- Wild, with target on ext4 or tmpfs. It was the fastest in everything I measured there, on both machines.
- mold, with target on btrfs, a kernel older than 7.2 and a machine like my desktop, if I do not want to touch flags. On the laptop not even then: Wild won on btrfs too.
- Wild with
--mmap-output-fileon btrfs with a kernel older than 7.2. On both machines it cut 18 to 37 % of its time. I would measure it on mine before making it permanent. - Moving
targetoff btrfs (to tmpfs, or to an ext4 volume) before switching linkers, if the link dominates the cycle. It is what moved the numbers in this post the most. - Replacing rust-lld with mold or Wild in debug with large binaries. In release and with projects like ripgrep or cargo, the link is still 1 to 4 % of the rebuild and I would change nothing.
Sources
- Wild beats mold linking Rust, the September 18 post with the bench I repeated.
- Wild 0.11.0 release notes: COMDAT, a 32-thread cap, fallocate and huge pages on ext4/xfs, mmap on btrfs with a new kernel.
- Wild, issue #1499 “BTRFS write performance” and PR #2509, with the kernel-dependent mode switch in
libwild/src/fs.rs. - Wild, BENCHMARKING.md, the method of saving a link and replaying it with
run-with. - mold 3.0.0 release notes: the Rust rewrite and performance on par with 2.42.1.
- The scripts, the sheet with its four amendments, the CSVs and the analysis:
lab/linkers2/in the repository and in this gist. - ripgrep, cargo, rust-analyzer and bevy, the linked projects.
Measured between October 25 and 27, 2026 on fedora (AMD Ryzen 7 7800X3D, NVMe, NixOS, kernel 6.18.55, /data on btrfs with compress=zstd:1) and on lenovo (Intel i5-8250U, SATA SSD, NixOS, kernel 6.18.55, ext4). rustc 1.99.1, GNU ld 2.46, Wild 0.10.0 and 0.11.0, mold 2.42.1 and 3.0.0, rust-lld from the toolchain. ripgrep 3fce3b5, cargo 8814ead, rust-analyzer 0c4d746, bevy fd9e760.
NEWSLETTER · MEASURED
A new measurement, when there is one.
Each post with its numbers, its failures and the scripts to repeat it. One to three emails a month. Unsubscribe with one click.
LATEST
- Unlimited-OCR 2 on 16 GBtie with Chandra 2, out of memory at 80 pages
- Wild vs mold on btrfsthe order flips with the disk
- 7 harnesses on MiniMax-M329 of 39 with the simplest harness
I run Listmonk on my own server; your address stays there. You confirm by email before getting anything.
Comments
No comments yet. The first one is yours.