对比对象是 uutils coreutils 0.12.0 的 x86_64-unknown-linux-musl 预编译包,和这台 Ubuntu 24.04 上 dpkg 装的 GNU coreutils 9.4。前者是一个 14MB 的多调用二进制,靠 argv[0] 分发:
mkdir -p /tmp/uu-bin
ln -s /path/to/coreutils /tmp/uu-bin/ls
ln -s /path/to/coreutils /tmp/uu-bin/date
/tmp/uu-bin/ls --version | head -1
/tmp/uu-bin/date --version | head -1ls (uutils coreutils) 0.12.0
date (uutils coreutils) 0.12.0后面命令里的 $UU 都指这个多调用二进制,调用方式是 $UU ls、$UU date。
工具覆盖
$UU --list | wc -l
dpkg -L coreutils | grep -E '^/usr/bin/' | wc -l106
105数量接近,但差集不为空。uutils 这边缺的是:
comm -23 <(dpkg -L coreutils | grep -E '^/usr/(bin|sbin)/' | xargs -n1 basename | sort) \
<($UU --list | sort)chcon
md5sum.textutils
runcon
stdbufchcon 和 runcon 是 SELinux 相关的,stdbuf 靠一个 libstdbuf.so 做 LD_PRELOAD 注入来改缓冲策略。这三个在没有 SELinux 的机器上用不到,但 stdbuf 不一样——不少构建脚本和日志管道用它来强行关掉子进程的块缓冲。如果你的流水线里有 stdbuf -oL,替换之前先把它挑出来。
md5sum.textutils 是 Debian 的历史遗留,忽略。
反过来 uutils 多出来的 hostname、kill、more、uptime 在 GNU 那边不属于 coreutils 包,分别来自 hostname、util-linux、procps。这是打包边界差异,不是功能差异,但意味着如果你用 uutils 的 symlink 目录整个覆盖 PATH,会顺手接管这四个本来由别的包管的命令。
输出一致的部分
大多数日常调用是对得上的。抽查了几组:
stat -c '%n %s %Y %a %U:%G' /etc/hostname
$UU stat -c '%n %s %Y %a %U:%G' /etc/hostname
md5sum /etc/hostname
$UU md5sum /etc/hostname
du -sh /usr/share/doc
$UU du -sh /usr/share/doc/etc/hostname 6 1790639989 644 root:root
/etc/hostname 6 1790639989 644 root:root
70d0b2e08bc614b5e2c81f1b0d4ec630 /etc/hostname
70d0b2e08bc614b5e2c81f1b0d4ec630 /etc/hostname
38M /usr/share/doc
38M /usr/share/docdate 的格式符也比我预期的完整,%N、%q、-d '3 days ago' 都正常:
$UU date +%Y-%m-%dT%H:%M:%S.%N
$UU date +%q
$UU date -d '3 days ago' +%Fsort -V(版本号排序)和 sort -h(人类可读单位排序)结果也和 GNU 一致。这两个是我原本最担心会出问题的,因为它们的语义细节很多。
输出不一致的部分:本地化
这是替换之后最容易一眼看出来的差别。这台机器的 locale 是 zh_CN.UTF-8。
locale | head -1
ls -l /etc/hostname
$UU ls -l /etc/hostname
df -h / | head -1
$UU df -h / | head -1
cat /nope
$UU cat /nopeLANG=zh_CN.UTF-8
-rw-r--r-- 1 root root 6 9月 29 07:59 /etc/hostname
-rw-r--r-- 1 root root 6 Sep 29 07:59 /etc/hostname
文件系统 大小 已用 可用 已用% 挂载点
Filesystem Size Used Avail Use% Mounted on
cat: /nope: 没有那个文件或目录
cat: /nope: No such file or directoryGNU 那边日期、df 表头、错误信息全部跟着 locale 走,uutils 一律输出英文。日期格式的差异尤其碍事——如果有脚本在解析 ls -l 的日期字段(本来就不该这么写,但存量脚本里确实有),格式变了就会静默出错。
错误信息不本地化对脚本没影响,对人有影响:中文环境的运维看惯了"没有那个文件或目录",突然全变英文,排障时的阅读速度会掉一截。
一处 uutils 更对的地方
cut -c 的语义是按字符切,但 GNU 的实现历史上是按字节切的:
printf '中文abc\n' | cut -c1-2
printf '中文abc\n' | $UU cut -c1-2?
中文GNU 那条切出来的是半个 UTF-8 序列,终端渲染成替换字符;uutils 正确返回了"中文"两个字符。这个差异在只处理 ASCII 的环境里永远不会暴露,但一旦数据里有中文,行为就分叉了。
我不建议把它当成"uutils 更好所以可以换"的理由。更现实的读法是:任何一处行为差异,都可能在某个存量脚本里埋着一颗定时炸弹,而你不知道是哪一颗。
速度
“Rust 写的所以更快"这个预期在这组数据上不成立。测试方法如下,每项跑三遍:
seq 1 2000000 | awk '{print 2000000-$1}' > /tmp/big.txt
head -c 300M /dev/urandom > /tmp/big.bin
t() { local s=$(date +%s%N); "$@" >/dev/null; local e=$(date +%s%N); echo $(( (e-s)/1000000 )); }
t /usr/bin/sort /tmp/big.txt ; t $UU sort /tmp/big.txt
t /usr/bin/sha256sum /tmp/big.bin ; t $UU sha256sum /tmp/big.bin
t /usr/bin/wc -l /tmp/big.txt ; t $UU wc -l /tmp/big.txt
t /bin/cat /tmp/big.bin ; t $UU cat /tmp/big.bin8 核、内核 6.8.0-149-generic,三遍的最好成绩(单位毫秒):
sort 2M 行 GNU=659 uu=1272
sha256sum 300M GNU=628 uu=1203
wc -l 2M 行 GNU=5 uu=5
cat 300M GNU=42 uu=10sort 和 sha256sum 上 uutils 慢了大约一倍,cat 上快了四倍,wc 打平。
这个对比有个必须说清楚的方法论问题:我用的 uutils 是 musl 静态构建,GNU 那边是发行版的 glibc 动态构建。两个变量混在一起了——差异可能来自实现,也可能来自 musl 的内存分配器、来自静态构建没有走上 CPU 特定的加速路径(sha256sum 慢一倍很像没用上 SHA-NI 指令)。要严格归因,得拿同一套 libc 的构建重测,或者去翻 uutils 的 feature 开关。我没做这一步,所以上面的数字只能读成"这个预编译包在这台机器上的表现”,不能读成"Rust 实现比 C 实现慢"。
cat 快四倍倒是个有意思的观察。GNU cat 在写非 tty 输出时有一套自己的缓冲逻辑,/dev/null 这个目标下两者的差异被放大了。这个数字同样受目标设备影响,换成管道或者真实文件结论可能不同。
我的判断
我不建议在生产机器上整体替换 /usr/bin 里的 coreutils。理由不是性能,是行为差异的排查成本:本地化输出、cut -c 语义、缺失的 stdbuf,每一项单独看都是小事,但它们分布在几百个存量脚本里,你没法一次性确认全部。
我倾向的用法是两个窄场景。
一是容器镜像。基础镜像里换成 uutils 的多调用二进制,能省掉 coreutils 包,而且镜像里通常没有依赖 locale 输出的脚本。风险面比宿主机小得多。
二是当作交叉环境的工具集。musl 静态构建的这个二进制不依赖目标机的 glibc 版本,扔进一个精简的 initramfs 或者救援环境里很省事。
三是逐个工具地引入,而不是整套替换。把 cut 这种你认为实现更正确的工具单独 symlink 进 PATH,其余保持 GNU。这样出问题的时候你知道该看哪个命令。
至于"什么时候可以整套换",我的标准是:本地化输出补齐、stdbuf 有等价物、并且在你的实际负载上做过同 libc 的性能对比。这三条现在的 0.12.0 都不满足前两条。
分情况的一点是发行版。有些发行版已经把 uutils 打进了仓库,走包管理器装的话会和 GNU coreutils 冲突或者需要 alternatives 机制配合,路径布局和上面这种手动 symlink 完全不同。用之前先确认你那条路径上有没有现成的包。