Rust 编译慢、target/ 目录大,这是老生常谈了。这篇记录一下我目前在用的几个改善手段,以我的项目 ClewdR(394 个 crate 依赖的异步 Web 服务)为例。

环境:Rust 1.94.1,CachyOS (Arch-based),NVMe SSD,Btrfs。

rust-lld

链接是 Rust 编译的最后一步,也是传统上最慢的一步。GNU ld 在这里表现很差,特别是开了 LTO 的时候。

以前的做法是手动装 lldmold,然后在 .cargo/config.toml 里配:

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

但从 Rust 1.85(2025-02-20)开始,rust-lldx86_64-unknown-linux-gnu 上已经是默认链接器了,不需要任何配置:

$ readelf -p .comment target/release/clewdr

String dump of section '.comment':
  [     1]  Linker: LLD 21.1.8
  [    5f]  rustc version 1.94.1 (e408947bf 2026-03-25)

升级 Rust 就行,免费午餐。

顺带一提,社区里还有两个值得关注的替代链接器:

  • mold:以速度为卖点的链接器,在非 LTO 场景下通常比 lld 更快,不过对 LTO 的支持有限。
  • wild:一个用 Rust 写的实验性链接器,目标是成为最快的 Linux ELF 链接器,做了大量多线程优化。目前还在活跃开发中,有兴趣可以关注。

对于大多数人来说,默认的 rust-lld 已经够用了。

sccache

sccache 是编译缓存,类似 ccache 但支持 Rust。它缓存每个 crate 的编译产物,相同输入直接复用,不再重新编译。

配置很简单,装好 sccache 后在 ~/.cargo/config.toml 加一行:

[build]
rustc-wrapper = "sccache"

以 ClewdR 的 release 构建为例(opt-level = "z", lto = true, codegen-units = 1):

场景耗时
无 sccache,clean build48.4s
sccache 冷缓存,clean build55.7s
sccache 热缓存,clean build34.2s

首次构建因为要写缓存会稍慢,之后 clean build 快了约 30%。热缓存下的 cache hit rate:

Cache hits rate                   52.00 %
Cache hits rate (Rust)            54.43 %
Cache hits rate (C/C++)           50.00 %

主要收益场景:cargo clean 后重建、切分支、多项目共享依赖。proc-macro crate 不会被缓存。本地缓存默认 10 GiB,也支持 S3 / GCS 远程缓存。

Btrfs 透明压缩

ClewdR 一次 release 构建产生 5121 个文件、840 MiB。多个 Rust 项目的 target/ 加起来轻松几十 GB。

Rust target 黑洞

我的文件系统是 Btrfs,挂载选项带了 compress=zstd:3

/dev/nvme0n1p2 on /home type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache=v2)

对上层完全透明,不需要改任何构建配置。用 compsizetarget/ 的实际磁盘占用:

Processed 5121 files, 7468 regular extents (7782 refs), 2710 inline.
Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL       41%      320M         768M         814M
none       100%       82M          82M          96M
zstd        34%      237M         685M         717M

768 MiB 的数据实际只占 320 MiB,不到原始大小的一半。编译中间产物(.o.rlib.rmeta)压缩效果尤其好。

有一点要注意:sccache 的缓存本身已经是压缩过的,Btrfs 再压几乎没有效果:

# ~/.cache/sccache/
Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL       99%      9.8G         9.8G          10G

zstd:3 在 NVMe SSD 上基本感受不到性能损失,是个不错的平衡点。

如果你用的是 ZFS,同样支持透明压缩,设置 compression=zstd 即可,效果类似。

文件去重

多个 Rust 项目之间的 target/ 目录往往有大量重复内容——相同版本的依赖编译出来的 .rlib.rmeta 文件是完全一样的。透明压缩能缩小单个文件的体积,但对这种跨项目的重复无能为力,这时候就需要文件系统级别的去重了。

我本地 18 个 Rust 项目的 target/ 目录合计 Referenced 51 GiB,经过 Btrfs 去重(reflink)+ zstd 压缩后,实际磁盘占用只有 15 GiB:

Processed 165605 files, 274042 regular extents (562708 refs), 101289 inline.
Type       Perc     Disk Usage   Uncompressed Referenced
TOTAL       47%       15G          32G          51G
none       100%      7.7G         7.7G         9.0G
zstd        30%      7.3G          24G          42G

其中去重把 51G 降到了 32G(省了约 37%),压缩再把 32G 降到 15G(省了约 53%),两者叠加效果相当可观。

Btrfs 支持离线去重(offline deduplication),原理是把内容相同的 extent 合并为同一份物理数据(reflink)。常用的工具有两个:

  • duperemove:扫描指定目录,找到重复的 extent 后提交给内核去重。适合手动或定时跑一次。
  • bees:后台守护进程,持续监控文件系统变化并自动去重。比 duperemove 更适合"设好就忘"的场景,但会持续占用一些 CPU 和内存。

ZFS 则内置了在线去重(inline dedup),设置 dedup=on 即可。比 Btrfs 的离线去重更激进——写入时就直接比对,重复数据根本不会落盘。代价是每个块都需要在内存中维护一条 DDT(Dedup Table)记录,数据量大的时候内存开销非常可观。一般建议内存充裕(比如 NAS / 服务器场景)再开,桌面机上慎用。

去重和前面的 sccache 看起来有点像,但侧重点不同:sccache 省的是时间,跳过重复编译直接从缓存取结果,但每个项目的 target/ 里该有的文件还是各自独立的副本;去重省的是空间,把这些内容相同的副本在磁盘上合并成一份。两者是互补的。

写在最后

Rust 编译时间和磁盘占用是社区诟病已久的问题。Rust Team 自己也在持续努力——rust-lld 默认启用、增量编译改进、前端并行化等等——但社区的反馈始终是"还不够快"。上面这些手段说到底都是 workaround,是在编译器本身没法一步到位的情况下,从外围能做的一些补救。而且老实说,即使全部用上,Rust 的编译体验对比大多数其他语言依旧几乎是最慢、空间占用最大的那一档。只能说,这就是为零成本抽象和所有权检查付出的代价吧。