
Rust Coreutils 0.12 vs GNU 9.12: almost everything works the same, starting a process costs nearly three times more, and mv between disks loses the dates
I measured Rust Coreutils (uutils) 0.12 against GNU coreutils 9.12 in containers with limits: the GNU test suite, 157 commands with their output compared, and process start-up. uutils is slower in 91 of 157 cells, takes 2.8 times longer to launch 5,000 processes, and mv across file systems loses the modification dates.
Rust Coreutils 0.12 came out on 17 September, and Phoronix reported that Ubuntu 26.10 completes its transition to the Rust utilities with that version. They are cp, mv, rm, ls, sort, cat and about a hundred more, the ones that run every time a script starts. The release notes carry two sentences I wanted to check. One is the compatibility figure: 653 GNU tests pass, 21 fail, 0 error. The other is new: a utility that is significantly slower than GNU now counts as a bug.
I measured both, with the same binaries under the same resource limits, and I also compared the output of every command.
The 653 did not reproduce in my container, but it is honest: it is an aggregate of six CI runs. On speed, uutils is slower in 91 of 157 cells and starting a process costs almost three times more. And in the commands Ubuntu already uses, only three GNU tests fail, but mv between file systems loses the modification date.
What uutils is
It is a rewrite of GNU’s coreutils in Rust, MIT licensed, that aims to be a drop-in replacement. The project has been at it for years; with 0.12 it focused on fixing what Ubuntu’s maintainers asked for: a data loss in parallel install -D, name collisions in cp -R, chmod -R through symbolic links and du on 32-bit architectures. It also stopped rm from crashing on very deep trees.
GNU coreutils 9.12 came out on 14 September, three days earlier. I measured speed against that version. For the tests I used the 9.11 ones, because that is what uutils’ scripts pin.
In the ubuntu:26.04 image I checked, /usr/bin/mv points to gnumv, which is GNU 9.7, and rust-coreutils 0.8.0 ships as a separate package. I do not know what a desktop with 26.10 installed will ship, because that version comes out in October.
Same data, same commands, same limits. What changes is the implementation. The pods never share a physical core, but they do share cache and memory bandwidth.
The bench
Everything ran on fedora, on a Ryzen 7 7800X3D. Timings were measured with the release binary in a container of two physical cores (0 and 1) and 4 GiB, no network, data generated inside with fixed seeds, stdout to /dev/null. The GNU suite ran with the release-small binary in another container of 4 cores (4 to 7) and 8 GiB, on the same host and at the same time: two different binaries and two different limits, not the same bench run twice. They do not share a physical core; they do share cache and memory bandwidth, and that is a condition of the measurement.
The binaries: GNU coreutils 9.12 that I built from the official tarball, and uutils 0.12.0 built with cargo build --release --features unix (upstream’s profile: lto = "fat", a single codegen unit, panic = "abort"). The GNU tests were run with the release-small profile, which is what its CI uses. I did not measure the binary Ubuntu installs, which may be built with other options.
To measure I used hyperfine 1.20.0, with both implementations interleaved in the same session, output to /dev/null, and between 10 and 300 repetitions per implementation, depending on the cell; ratios may change when writing to a file or a pipe. Each cell also compares the output: the hash of what the command writes and of the resulting file tree.
The GNU suite: the 653 is an aggregate
The release notes say 653 tests pass, 21 fail, 0 error and 18 skipped, out of 692. The repository where uutils publishes its results confirms it: the run of 16 September gives exactly those figures. But it is the best result per test, combining six CI runs (user, root, terminal, one VM with SELinux and another with SMACK), and the run for the exact commit of the tag gives 653, 23 and 16. Between 16 and 19 September there are 40 records with the same 692 tests, with 646 to 655 passing; seven other records from those days use a different suite size. Because the commits change from one run to the next, that spread neither isolates nor bounds the measurement noise: it is just the range I observed.
I ran it myself, without root, with the container’s four cores: 569 or 570 pass, 27 or 28 fail and 95 are skipped, in four nearly identical runs. Combining user, root and terminal as its CI does, 590 pass, 29 fail and 73 are skipped. As a control, I ran the same tests with real GNU 9.11: 636 pass, 4 fail and it skips another 95 tests, 90 of them the same as mine.
- Release notes (aggregate of 6 runs)653 pass · 21 fail · 18 skip of 692The same figures its tracking repository publishes for the 16 September run.
- Its CI, run of the exact tagged commit653 pass · 23 fail · 16 skip of 692
- My aggregated run (user, root and terminal)590 pass · 29 fail · 73 skip of 692No SELinux, SMACK, mount or mkfs.
- My strict run, no root570 pass · 27 fail · 95 skip of 692Four runs: 569 or 570 pass.
- Control: real GNU 9.11, strict run636 pass · 4 fail · 95 skip of 735Out of 735 tests: my control uses the GNU tree without uutils' edits.
Almost all of the distance between the notes and my run is tests skipped because of my environment's restrictions: 21 needed mount, mkfs or chattr, 17 a locale, 9 SELinux. Real GNU skips them too.
The 95 skipped are the number that matters. The control with real GNU skips them too: with no SELinux, no mount, no mkfs and no en_US.UTF-8 in the image, some tests cannot run. What separates my 590 from its 653 is mostly that, and what separates my 570 from my 590 is the root phase. That is why I say the figure does not reproduce, not that it is false.
Of its 21 published failures, 17 also fail in my run. 12 tests fail here that its CI does not mark, and I separated them one by one:
- One failure from memory exhaustion under the container limit.
od/big-w: uutilsodreserves about 14.8 GB of virtual memory where GNU streams, and the kernel’sjournalctlrecords about twenty out-of-memory kills in the 8 GiB container. In a separate run, outside the matrix, it does finish, but in 154 to 170 s against 11 s for GNU. - Four from the environment.
id/context,id/no-context,runcon/runcon-computeandruncon/runcon-no-reorderneed SELinux, which was not available in this test environment. Its CI passes them in a separate VM. - One flaky, one that changes between commits.
tail/tail-n0ffailed in two runs and passed in two others.tail/symlinkpasses in the release-notes run and fails in the tagged-commit run; that alone is not enough to call it flaky. - One that depends on load.
sort/sort-compress-proc, which I cover below. - Four that look like real differences.
cut/bounded-memory: uutilscutaborts when asking for 8 MiB under the same memory limit GNU passes with.pr/bounded-memory:prruns out of memory even with 1 GB of headroom.shuf/shuf: ifgetrandomfails with ENOSYS,shufpanics with exit code 134 and GNU exits with 1.touch/now-owned-by-other, which I also cover below. In the release-notes runcutandprcount as skipped; andshufprobably passes in its CI because the runner ships nostraceand that check never runs (I did not read that log, it is an inference).
In short: my 590, 29 and 73 against its 653, 21 and 18 are explained by 55 environment skips and one memory artefact. Four real failures that its CI does not list remain, and one that depends on load.
In what Ubuntu already uses, three tests fail
I looked at the failures of the utilities Ubuntu ships today: cp, mv, rm, install, ls, sort, du, chmod, ln, mkdir, touch and stat. Only three tests fail.
sort/sort-compress-proc. If the compression program exits before reading all its input, uutilssortcan die from SIGPIPE or exit 0, and GNU exits with 2 andclose failed: Broken pipe. It depends on load: it passed alone on an idle machine and failed with the CPU busy.touch/now-owned-by-other, root phase only.touch -d now FILEgives “Permission denied” if you can write to the file but do not own it, because uutils passes an explicit timestamp instead ofUTIME_NOW.rm/rm-readdir-fail. Whenreaddir()fails halfway, GNU writestraversal failedand uutilscannot remove 'dir': Directory not empty. Both exit with 1; the diagnostic changes. It is on the list of tests uutils declares unfixable.
This is not a clean bill of health. In those same utilities, seven cp tests, two mv, four rm, two install, three ls, three sort, two du and five mkdir tests were skipped, because of the environment. They are not evidence either way.
Speed: 157 cells
The matrix has 157 cells in five groups: process start-up, hashes, text, file trees and disk. Each one compares uutils’ median against GNU’s, with the range between the fastest and slowest repetition. With that strict criterion, uutils turned out slower in 91 cells, with no measurable difference in 21 and faster in 45. In 56 cells it took more than 1.5 times what GNU did (51 with the same output), and in 17 it took less than two thirds.
- uutils slower 91
- no measurable difference 21
- uutils faster 45
- different output
Hover over a point to see its cell. The dashed lines mark 1.5 times slower and 1.5 times faster. Hollow rings are cells where uutils' output differs from GNU's: their time is not comparable.
By group, the geometric mean of the ratio was 2.73 for start-up, 1.66 for trees, 1.27 for disk, 1.10 for text and 1.05 for hashes.
Starting a process
A single true took 0.29 ms with GNU and 0.94 ms with uutils, about 3.2 times. It sounds like nothing, until you remember a script launches thousands. With 5,000 launches from a shell loop, true took 1.78 s against 5.03 s, expr 2.60 s against 5.51 s and basename 2.09 s against 5.52 s.
Real time of a shell loop that launches the command 5,000 times.
Real time, 50,000 files of 4 KiB in tmpfs (cp) and a directory of 100,000 entries (ls).
Real time, 1 million lines, en_US.UTF-8 locale.
Real time. wc -m and cut -c in UTF-8, sort -g -k3 and b2sum in the C locale.
Each lane fills in the time that implementation took, using the median of its repetitions. Switch scenarios with the buttons.
Why? What I can measure closely is this. The uutils binary weighs 15 MB because it is a single piece for the 106 utilities, and for a true it costs 80 system calls against GNU’s 29. Around 65% of the dynamic loader’s time goes into applying about 26,300 relative relocations. The exact cycle count varies by about 10% between runs; the stable part is that order of magnitude.
Where it loses by a lot
pasteof two files of a million lines: 12.6 times slower. uutils makes 1,000,000writecalls and GNU 4,114. A later check, with the output going to a pipe instead of/dev/null, gave about 19.7 times; it stands as an observation, not a proven lower bound.sortinen_US.UTF-8: 0.48 s with GNU and 2.30 s with uutils, 4.8 times. In theClocale there is no loss: 0.21 s against 0.19 s. GNU uses about two threads (0.82 s of CPU for 0.48 s of wall time) and uutils in UTF-8 is sequential, so the 4.8 holds for my 2-CPU pod; per CPU second, about 3 times.cp -randcp -aof a 50,000-file tree: 2.4 and 2.9 times slower in tmpfs. (On ext4 they also come out slower, but those cells are very noisy and I do not quote them.)mv -tof 5,000 files: 6 times.factorover 100,000 numbers: 3.8 times, with 100,000writecalls against 1,064.fmt -w 72: the memory peak, in a single run, was about 912 MiB with uutils against 1.6 MiB with GNU.
Where it wins
wc -m in UTF-8 is 14 times faster, head -c 4.7 times, cut -c 4.5 times, sort -g -k3 2.9 times in the C locale, and b2sum of 1 GiB 1.2 times. And in hashes such as sha256sum of a 1 GiB file there is no measurable difference.
When the output differs
Twenty of the 157 cells gave a different output from GNU’s, and their times do not count. I checked each difference with a minimal command.
| Command | GNU 9.12 | uutils 0.12.0 |
|---|---|---|
mv from tmpfs to ext4 | keeps the modification time | sets the time of the move |
wc -m with LC_ALL=C | counts bytes | counts UTF-8 characters |
numfmt --to=si --format=%.2f with 143070000 | 143.08M | 143.07M |
timeout -s KILL on a hung process | exits with 137 | exits with 124 |
mkdir -m u=rwx,g+s | creates with mode 2777 | creates with mode 777 |
base64 -d with invalid input | writes what it decoded and fails | fails without writing anything |
date -d '2021-03-04 next friday' | 4 March | 5 March |
The one that weighs most is mv. When moving across different file systems, uutils 0.12.0 does not keep the modification time: in my test, after moving a tree of 50,502 entries, 50,501 ended up with the time of the move, and the same with a 1 GiB file. It is an issue open in uutils since April. In a hand check outside the matrix, the 0.8.0 that ships in the Ubuntu 26.04 image does not keep it either, although there mv is still GNU’s. For an rsync or a backup that depends on dates, it is a change of behaviour that gives no warning.
Six of the 20 different-output cells are ls: ls -l showed a + where GNU puts . (and ? with -Z) on files labelled only with SELinux. That comes from my build not having the SELinux option, not from uutils in general; they are in the count of 20, but I do not treat it as a behaviour difference of the project.
What I cannot claim
- How the binary Ubuntu installs performs. I measured upstream’s
release. If Ubuntu builds with other options, start-up and sizes may change. - That my figures hold on another processor. The 7800X3D has 96 MiB of L3 cache and my 97.6 MB text file almost fits in it whole. The text cells do not represent an ordinary processor.
- That the frequency was fixed. The governor was on
powersave: I measured between 4.39 and 4.42 GHz at the start and between 3.79 and 4.02 at the end. - The cause of almost all the slow cells. I only have measured evidence in
pasteandfactor(thewritecalls per line) and in start-up. For the rest I know how much, not why. - The tie-break on the 12 tests that fail here and not in its CI. I did not run the SELinux or SMACK phases.
- Anything about cold reads or other file systems. I had no root to drop caches.
My opinion
I expected uutils to pass the tests and lose on speed, and both came out half true. The 653 is a CI number I cannot reproduce without a VM with SELinux, but in the utilities that matter the tests fail little and for odd reasons, within what I could test without SELinux or SMACK. And on speed it is not an even loss: on text it is almost tied, it wins several cases by a lot and loses by a lot in about a third of the cells, almost always in the same places.
What I did not expect is mv: a silent behaviour difference, with an issue open since April, in a tool Ubuntu is about to make the default. The project says being significantly slower is a bug. With these numbers it has a concrete list to start from: start-up, paste, sort in UTF-8 and cp.
When I would use it
- Yes: on a server or a container where commands are used little and not launched in long loops, or on a desktop that already ships it by default.
- Yes, carefully: if your scripts launch thousands of processes or move trees between disks. Measure with your own commands first.
- Not yet: where modification dates must survive
mvbetween disks, or where you sort UTF-8 text at large scale, untilmvis fixed and thesort4.8 is closed.
Sources
- Rust Coreutils 0.12.0, release notes of 17 September 2026.
- Rust Coreutils 0.12 Released With Fixes Sought By Ubuntu, Phoronix, 17 September 2026.
- coreutils-9.12 released, GNU, 14 September 2026.
- uutils issue 11743,
mv: preserve mtime/atime when moving across filesystems, opened on 10 April 2026. - hyperfine 1.20.0, the measuring tool.
- GNU coreutils, the reference implementation.
Comments
No comments yet. The first one is yours.