给链接器做性能画像:Rust 构建慢,到底是不是链接器的锅?

本文是对 Profiling linkers 的整理与翻译。


内容结构概览

本文主要围绕“如何给链接器做性能分析”展开,重点包括:

  1. 背景:为什么作者开始关注 Rust 构建里的链接器性能
  2. moldlld 的开发者为什么会来询问
  3. 为什么之前测试里 moldlld 差异不大
  4. 如何用 mold -run cargo build
  5. 如何确认最终二进制确实是用 mold 链接的
  6. 为什么 mold -perf 不能直接和 mold -run 搭配
  7. 如何写一个 wrapper script 来收集 mold profiling 数据
  8. GNU ld、mold、lld 在一个 Rust release hot build 里的对比
  9. linker driver 和真正的 linker 有什么区别
  10. 为什么 GCC 的 -fuse-ld=/path/to/wrapper 会失败
  11. 为什么换成 clang 作为 linker driver 就可以
  12. mold 的 profiling 输出怎么看
  13. 如何用 lld --time-tracelld 做 profiling
  14. 如何用 jqlld 的 JSON trace 里提取耗时
  15. 为什么 “完整构建时间” 和 “链接器自报时间” 不是一回事
  16. LTO 开启后,GNU ld 和 lld 的结果为什么变得奇怪
  17. fat LTO 和 thin LTO 的构建时间差异
  18. 这篇文章真正想告诉我们的:不要猜,先测量

一、背景:Rust 构建慢,链接器到底占多少?

这篇文章延续了作者之前的一篇文章:为什么我的 Rust 构建这么慢?

在那篇文章之后,moldlld 这些链接器的开发者找到了作者,想知道为什么使用他们的链接器没有带来非常明显的提升。

答案其实很简单:

当一个项目本身没有太多链接工作要做时,换链接器当然不会带来数量级提升。

在作者之前的项目里,moldlld 之间差距不到一秒,而 GNU ld 明显慢一些,大概落后到 4 秒左右。

但这并不代表链接器不重要。
恰恰相反,链接器开发者关心这些差异,说明“链接器性能”本身就是一个值得优化、值得测量的领域。

于是作者得到了一些来自 moldlld 开发者的建议:如何 profile 这些链接器。

这篇文章就是一次小型实验记录:
不是泛泛而谈哪个链接器快,而是实际跑命令、改 .cargo/config.toml、写 wrapper script、收集 profiling 输出、对比 release build 和 LTO build。


二、先说结论:这不是一篇“mold 吊打 lld”的文章

很多性能文章容易被读成排行榜:

第一名:mold
第二名:lld
第三名:GNU ld

但这篇文章不是这么简单。

作者真正展示的是:

  • 在普通 release hot build 中,GNU ld 大约 7 秒;
  • 换成 mold 后,大约 5.1 秒;
  • 换成 lld 后,大约 5.2 秒;
  • 但链接器自己报告的真实链接时间只有几百毫秒;
  • 完整构建时间里还有很多不是链接器本身的工作;
  • 开启 LTO 后,构建时间主要被 LTO 影响,普通链接器 profiling 反而变得不那么能解释问题。

所以这篇文章的重点不是“谁赢了”,而是:

当你怀疑链接器拖慢构建时,应该怎么让链接器自己告诉你它在做什么。


三、mold -run cargo build:最简单的 mold 用法

作者平时使用 mold 的方式很简单:

mold -run cargo build

这表示:用 mold 包装这次 cargo build,让构建过程中最终链接阶段使用 mold

示例输出大概是:

$ mold -run cargo build
   Compiling salvage v1.4.0 (/home/amos/bearcove/salvage)
    Finished dev [unoptimized + debuginfo] target(s) in 1.74s

但“命令跑了”不代表你真的用了 mold

所以作者接着用 readelf 检查 ELF 文件的 .comment section。


四、如何确认二进制真的是用 mold 链接的?

作者用这个命令:

readelf -p .comment target/debug/salvage

输出里能看到类似:

String dump of section '.comment':
  [     0]  GCC: (GNU) 11.2.1 ...
  [    2e]  mold 1.0.0 (...)
  [    89]  GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
  [    b3]  clang version 12.0.1

这里出现了 mold 1.0.0,说明最终二进制确实包含了 mold 写入的注释信息。

这个检查很有用。

因为构建链路里有很多层:

cargo
  ↓
rustc
  ↓
cc / clang 这类 linker driver
  ↓
真正的 linker:GNU ld / lld / mold

你以为自己换了链接器,实际可能只是参数没生效。
所以先验证最终产物很重要。


五、mold -perf:mold 自带的 profiling 开关

mold 有一个 profiling 选项:

mold -perf

它可以输出 mold 内部各阶段耗时。

问题是,作者没有找到办法把 -perfmold -run 直接结合起来。

于是他做了一个很自然的工程处理:写 wrapper script。


六、写一个 mold-wrapper

作者在 ~/mold-wrapper 里写了这个脚本:

#!/bin/bash
mold -perf "$@" 2>&1 >> /tmp/mold-log.txt

这段脚本做了几件事。

第一,调用真正的 mold

mold -perf "$@"

"$@" 表示把 wrapper 收到的所有参数原样传给 mold

第二,打开 mold 的 profiling:

-perf

第三,把输出追加到日志文件:

>> /tmp/mold-log.txt

第四,把标准错误重定向到标准输出:

2>&1

为什么要这么做?

因为很多编译和链接工具会把诊断信息输出到 stderr。
如果只抓 stdout,可能抓不到 profiling 输出。

这个 wrapper 的目的就是:
让 Cargo/Rust 调链接器时,实际调到 wrapper,然后 wrapper 再调用 mold -perf,把结果写到 /tmp/mold-log.txt


七、换一个更适合测试的项目:salvage

作者之前的 futile 项目里链接时间差异不明显,于是这次换成另一个自己的项目:salvage

而且这次测试 release build,而不是 debug build。

测试方式是:

  1. 先完整构建一次
  2. 修改 main.rs
  3. 再执行 cargo build --release
  4. 重复几次
  5. 看 hot build 时间

这里的 hot build 指的是:
大部分依赖已经构建好,只改了一点点自己的代码,然后重新构建。
这类场景对日常开发体验非常重要。

因为你写代码时,最常见的不是 clean build,而是:

改一行
编译
运行
再改一行
再编译

所以 hot build 的链接时间很值得关注。


八、GNU ld baseline:大约 7 秒

首先使用默认链接器,也就是 GNU ld。

.cargo/config.toml 大概是:

[target.x86_64-unknown-linux-gnu]
rustflags = []

然后反复修改 main.rs 并运行:

cargo build --release

几次结果大概是:

6.99s
7.04s
6.91s

所以作者把 7 秒作为 baseline。

这不是整个项目从零构建的时间,而是改动后重新 release build 的时间。


九、第一次配置 mold-wrapper:失败了

接着作者尝试在 .cargo/config.toml 里这样配置:

[target.x86_64-unknown-linux-gnu]
rustflags = [
    "-C", "link-arg=-fuse-ld=/home/amos/mold-wrapper",
]

结果失败:

cc: error: unrecognized command-line option '-fuse-ld=/home/amos/mold-wrapper'

这就是文章里最有价值的踩坑点之一。

表面看,我们只是把 -fuse-ld 指向一个 wrapper 脚本。
为什么不行?

原因是:这里有两层“链接器”。


十、linker driver 和真正的 linker,不是同一个东西

作者在这里解释了一个很容易混淆的概念。

“linker” 这个词可能指很多东西。

运行程序时,有动态链接器,比如 Linux 上的 dynamic linker。
构建程序时,有 build-time linker。

但即使在 build-time linker 场景里,还可以继续拆:

linker driver
actual linker

默认情况下,Rust 构建时可能会调用 cc 作为 linker driver。
这里的 cc 通常由 GCC 提供。

cc 这个 driver 接收一堆参数,然后再去调用真正的链接器。
比如你可以传:

-fuse-ld=lld

告诉它使用 lld

问题是,GCC 对 -fuse-ld= 的值有 allowlist。
它接受一些已知名字,比如 bfdgoldlldmold 等,但不一定接受一个任意路径。

所以:

-fuse-ld=/home/amos/mold-wrapper

在 GCC driver 这里被拒绝了。

这不是 mold 的问题,也不是 Cargo 的问题,而是 linker driver 的行为。


十一、换成 clang 作为 linker driver

解决办法是换成 clang 作为 linker driver。

clang-fuse-ld=/path/to/linker 更灵活,可以接受一个路径,包括 wrapper script。

于是配置改成:

[target.x86_64-unknown-linux-gnu]
linker = "clang"

rustflags = [
    "-C", "link-arg=-fuse-ld=/home/amos/mold-wrapper",
]

这次构建成功。

几次 hot release build 的耗时大概是:

5.17s
5.12s
5.10s

相比 GNU ld 的 7 秒,快了接近 2 秒。

这说明:
在这个项目、这个机器、这个配置下,换成 mold 确实有明显改善,但不是“构建时间归零”。


十二、mold profiling 输出怎么看?

接着作者打开 /tmp/mold-log.txt,里面是 mold -perf 输出的各阶段耗时。

大致会看到类似这样的条目:

User    System   Real    Name
0.800   5.360    0.392   all
0.171   0.515    0.177   read_input_files
0.068   2.334    0.101   register_section_pieces
0.564   2.512    0.114   total
...
0.184   2.407    0.091   open_file
0.102   2.400    0.084   copy
...

这里有几个字段:

  • User:用户态 CPU 时间
  • System:内核态 CPU 时间
  • Real:墙钟时间,也就是现实经过时间
  • Name:链接器内部阶段名称

mold 自报 real 总时间约 392ms。

这非常快。

但注意:前面 cargo build --release 总耗时是 5 秒多。
这说明整个构建过程里,真正由 mold 执行的链接阶段只占一小部分。

这正是这篇文章的核心提醒:

不要把完整构建时间全部归咎于链接器。链接器只是构建 pipeline 的一个阶段。

如果 mold 的 profiling 显示某个 pass 特别慢,那可以拿这些数据去给 mold 提 issue。
但在这次结果里,没有明显跳出来的异常阶段。


十三、继续测 lld

接着作者测 lld

他写了另一个 wrapper script:

#!/bin/bash
ld.lld --time-trace --time-trace-file=/tmp/lld.time-trace.json "$@"

这里用到的是 lld--time-trace 选项。

它会生成一个 JSON 文件:

/tmp/lld.time-trace.json

这个文件类似 Chrome tracing 的格式,可以被工具打开,也可以用 jq 直接提取统计数据。

作者再次跑几次 hot release build,结果大概是:

5.27s
5.26s
5.19s

mold 的 5.1 秒非常接近。

所以在这个实验里:

GNU ld    约 7 秒
mold      约 5.1 秒
lld       约 5.2 秒

但再次强调:这是完整构建时间,不是纯链接时间。


十四、用 jq 提取 lld 的 trace 数据

lld 的 time trace 是 JSON。

维护者建议作者用 jq 提取里面包含 Total 的条目:

cat /tmp/lld.time-trace.json \
  | jq -r '.traceEvents[] | select(.name|contains("Total")) | "\(.dur/1000000) \(.name)"'

输出大概是:

0.449559 Total ExecuteLinker
0.445119 Total Link
0.193442 Total Write output file
0.074866 Total Parse input files
0.061933 Total Merge/finalize input sections
0.028164 Total Split sections
0.023066 Total markLive
0.022392 Total Scan relocations
0.009324 Total Assign sections
...

也可以只看包含 Write 的条目:

cat /tmp/lld.time-trace.json \
  | jq -r '.traceEvents[] | select(.name|contains("Write")) | "\(.dur/1000000) \(.name)"'

得到:

0.193442 Write output file
0.193442 Total Write output file

这说明,在这次 lld 自报的数据中,整个链接大约 449ms,其中写输出文件大约 193ms。

作者顺手开了一个玩笑:
这其实是 useless use of cat。也就是说,上面的命令可以写成:

jq -r '...' /tmp/lld.time-trace.json

不需要 cat file | jq
但作者说,这样可以让 Reddit 网友在完美文章里挑点东西吐槽。

这就是原文的风格:严肃测性能,中间穿插一点开发者社区玩笑。


十五、moldlld 的自报时间差多少?

在这次实验里:

mold 自报 real time: 约 392ms
lld 自报 linker time: 约 449ms

差异大约 60ms。

这不是什么夸张差距。
完整构建时间层面,二者都在 5 秒多,差距更不明显。

所以作者没有夸大结论。
他只是说:这里没有明显异常,lldmold 自报慢大约 60ms。

真正的问题是:
如果完整构建要 5 秒,而链接器只用了 0.4 秒,那么剩下的时间在哪里?

可能在 rustc,可能在 codegen,可能在 LLVM 优化,可能在文件系统,可能在增量构建判断,可能在其他步骤。

这就是性能分析的基本原则:

测量一个阶段之前,先确认瓶颈真的在这个阶段。


十六、接下来测 LTO

前面测的是普通 release build。
接着作者问:LTO 怎么样?

LTO 是 Link-Time Optimization,链接时优化。

它的基本思路是:
普通编译时,各个 crate / object file 可能分别优化;
LTO 则在链接阶段或接近链接阶段,把更多程序整体信息拿出来,让优化器跨模块优化。

这可能带来更好的运行性能、更小二进制,或者更好的内联机会。
但代价是构建时间明显增加。

作者这里测试两种:

[profile.release]
lto = "fat"
debug = 1

以及:

[profile.release]
lto = "thin"
debug = 1

其中 debug = 1 表示 release build 里保留一定调试信息。


十七、fat LTO:构建时间直接到 1 分 10 秒

先测试 GNU ld + fat LTO。

几次 hot release build 结果都是:

1m10s
1m10s
1m10s

作者直接感叹:

LTO is not free.

也就是说,LTO 不是免费的。

这句话很重要。

很多人看到 LTO 可能会觉得它是 release build 必开选项。
但如果你在开发过程中频繁跑 release build,或者 CI 对时间很敏感,fat LTO 会让构建时间急剧上升。

从 7 秒到 70 秒,不是“小优化变慢一点”,而是工作流体验完全不同。


十八、fat LTO + lld:结果几乎一样

接着作者用 lld 测 fat LTO。

结果也是:

1m10s
1m09s
1m10s

这让作者怀疑:

GNU ld 在这里真的参与了吗?

因为 Rust LTO 看起来更像是 LLVM 特有流程。
如果 LTO 的主要工作发生在 LLVM 内部,而不是传统链接器本身,那么换 GNU ld 或 lld 可能对总耗时影响很小。

接着作者看 lld 的 time trace,结果更奇怪。

trace 里显示:

0.242555 Total ExecuteLinker
0.239797 Total Link
0.131408 Total Write output file
...
0.000359 Total LTO

这显然不符合直觉。

如果整个构建花了 1 分 10 秒,而 lld 只报告 0.24 秒,并且 Total LTO 几乎为零,那说明我们测到的并不是那 70 秒里真正消耗时间的部分。

作者也承认:这看起来不对。
要么测错了东西,要么真正工作发生在其他地方。

这段非常重要,因为它展示了性能分析中很常见的坑:

工具给出的数据是真的,但你以为它覆盖的范围可能是假的。

lld --time-trace 只告诉你 lld 自己内部的时间。
如果 LTO 的重活发生在 rustc/LLVM 其他阶段,lld 的 trace 当然看不到。


十九、thin LTO:比 fat LTO 快很多,但仍然比普通 release 慢

最后测试 thin LTO。

配置是:

[profile.release]
lto = "thin"
debug = 1

构建结果大概是:

14.55s
14.73s
14.95s

相比 fat LTO 的 70 秒,thin LTO 快很多。
但相比普通 release build 的 5 到 7 秒,仍然慢不少。

也就是说:

普通 release build:约 5-7 秒
thin LTO:约 15 秒
fat LTO:约 70 秒

这是非常实用的结论。

如果你只是想要一些跨 crate 优化,又不想让构建时间爆炸,thin LTO 可能是更现实的折中。

但是否值得,仍然要看项目:

  • 最终二进制性能提升多少?
  • 二进制大小是否减少?
  • CI 时间是否可接受?
  • 发布频率如何?
  • 开发时是否经常构建 release?
  • 是否可以只在正式发布时开 LTO?

LTO 是一个工具,不是默认答案。


二十、thin LTO 下的 lld trace 也“可疑”

作者再次查看 lld time trace。

结果显示:

0.388797 Total ExecuteLinker
0.369694 Total Link
0.175247 Total Write output file
0.059522 Total Merge/finalize input sections
0.043918 Total Parse input files
...
0.001439 Total LTO

但完整构建时间是 14 到 15 秒。

所以作者判断:

这些数据还是很可疑。要么测的不是正确部分,要么主要工作在别处完成。

这并不是文章失败了。
反而是非常真实的 profiling 体验。

很多时候你开始测一个东西,以为可以得到清晰答案:

这个函数慢
这个模块慢
这个 pass 慢

结果发现工具只覆盖了一部分系统,或者你测到的是外层的一小段。

这时候不能硬解释数据。
正确做法是承认测量边界,然后继续找更合适的测量点。

作者最后也说:肯定会有人指出他哪里测错了。


二十一、这篇文章的命令整理

为了方便理解,把文中主要命令和配置按用途整理一下。

1. 用 mold 包装 cargo build

mold -run cargo build

2. 检查二进制 .comment section

readelf -p .comment target/debug/salvage

3. mold profiling wrapper

#!/bin/bash
mold -perf "$@" 2>&1 >> /tmp/mold-log.txt

4. GNU ld baseline 配置

[target.x86_64-unknown-linux-gnu]
rustflags = []

5. 错误示范:GCC driver 不接受任意 -fuse-ld 路径

[target.x86_64-unknown-linux-gnu]
rustflags = [
    "-C", "link-arg=-fuse-ld=/home/amos/mold-wrapper",
]

可能报错:

cc: error: unrecognized command-line option '-fuse-ld=/home/amos/mold-wrapper'

6. 正确做法:用 clang 作为 linker driver

[target.x86_64-unknown-linux-gnu]
linker = "clang"

rustflags = [
    "-C", "link-arg=-fuse-ld=/home/amos/mold-wrapper",
]

7. lld profiling wrapper

#!/bin/bash
ld.lld --time-trace --time-trace-file=/tmp/lld.time-trace.json "$@"

8. 从 lld trace 中提取 Total 项

jq -r '.traceEvents[] | select(.name|contains("Total")) | "\(.dur/1000000) \(.name)"' /tmp/lld.time-trace.json

9. 从 lld trace 中提取 Write 项

jq -r '.traceEvents[] | select(.name|contains("Write")) | "\(.dur/1000000) \(.name)"' /tmp/lld.time-trace.json

10. fat LTO 配置

[profile.release]
lto = "fat"
debug = 1

11. thin LTO 配置

[profile.release]
lto = "thin"
debug = 1

二十二、这些测试数字该怎么读?

把原文里的关键结果整理成表格,大致是:

场景完整 hot release build 时间链接器自报时间
GNU ld,普通 release约 7 秒未测
mold,普通 release约 5.1 秒约 392ms
lld,普通 release约 5.2 秒约 449ms
GNU ld,fat LTO约 1分10秒不清楚
lld,fat LTO约 1分09秒到1分10秒trace 显示约 240ms,但可疑
lld,thin LTO约 14.5 到 15 秒trace 显示约 390ms,但可疑

这些数字不能简单推广到所有项目。

因为链接时间受到很多因素影响:

  • 项目规模
  • crate 数量
  • object file 数量
  • debug info 大小
  • 是否开启 LTO
  • 是否开启 incremental
  • CPU 核心数
  • 磁盘速度
  • 文件系统缓存
  • 链接器版本
  • Rust 版本
  • linker driver
  • 目标平台
  • C/C++ 原生依赖
  • 是否静态链接
  • 是否生成大量符号表和调试段

所以真正应该学的是方法,不是死记数字。


二十三、为什么完整构建时间和链接时间差这么多?

这是文章里最值得反复强调的一点。

当你执行:

cargo build --release

终端最后显示:

Finished release target(s) in 5.17s

这 5.17 秒不是链接器时间。

它包括整个 Cargo/Rust 构建流程里发生的所有事情:

Cargo 判断哪些 crate 需要重建
  ↓
调用 rustc
  ↓
解析、类型检查、borrow check
  ↓
MIR 优化
  ↓
LLVM codegen
  ↓
生成 object files
  ↓
调用 linker driver
  ↓
真正 linker 合并对象、解析符号、写输出
  ↓
构建完成

链接器只是最后一段。

如果最后一段只有 0.4 秒,那么换链接器最多也只能影响这 0.4 秒附近的部分。
除非原链接器真的非常慢,比如 GNU ld 比 mold/lld 慢几秒,否则不会让整个构建发生数量级变化。

这就是 Amdahl 定律的直觉版本:

你只能优化程序实际花时间的地方。
如果某个阶段只占 10%,即使把它优化到 0,也只能省 10%。

在这篇文章里,普通 release hot build 从 GNU ld 的 7 秒降到 mold/lld 的 5 秒,说明链接器确实有影响。
但 mold 和 lld 之间只有几十毫秒级别差异,因此对完整构建时间影响很小。


二十四、linker driver 这个坑为什么重要?

很多 Rust 用户想换链接器时,会直接搜索配置,然后复制:

rustflags = ["-C", "link-arg=-fuse-ld=lld"]

或者:

rustflags = ["-C", "link-arg=-fuse-ld=mold"]

这对常见名字可能没问题。

但当你需要插入 wrapper script、profile 工具、日志收集、特殊 linker path 时,就会遇到 driver 限制。

GCC 提供的 cc 不接受任意路径。
Clang 更灵活,可以接受路径。

所以,理解这一层很重要:

Cargo/Rust 不是直接调 ld
它可能先调 cc/clang 作为 linker driver
driver 再调真正链接器

这也解释了为什么错误信息来自 cc

cc: error: unrecognized command-line option ...

你想调的是 mold,但先拦住你的是 cc

这种问题在构建系统里很常见:
你以为自己在配置 A,实际失败发生在 A 前面的 B。


二十五、为什么 moldlld 对 Rust 开发者重要?

Rust 项目常被吐槽编译慢。
其中一部分时间来自编译器本身,另一部分可能来自链接。

尤其在大项目、debug info 很多、原生依赖复杂、release build、test binary 多、workspace 大的情况下,链接器可能变成比较明显的开销。

传统 GNU ld 很成熟,但不一定最快。
lld 是 LLVM 项目里的链接器,速度通常不错,也和 Rust/LLVM 生态关系自然。
mold 则是专门追求高速的现代链接器,在很多场景下非常快。

但对开发者来说,重点不是信仰某一个链接器,而是:

当你的构建慢时,知道有哪些替代链接器可以试;
当你试了以后,知道如何确认它真的生效;
当效果不明显时,知道如何 profile,而不是盲目猜。


二十六、LTO 的结论:它可能比链接器本身更值得关注

这篇文章的后半部分很有意思。

原本标题是 profiling linkers,但一开启 LTO,事情就不再是普通 linker 性能问题了。

fat LTO 把 hot build 从几秒拉到 1 分 10 秒。
这说明,在作者的项目里,如果你关心 release build 速度,LTO 可能比选择 GNU ld / lld / mold 重要得多。

lld 的 trace 却没有显示大量 LTO 耗时,说明 LTO 的主要工作可能发生在链接器外部,或者至少不在作者当前测量工具覆盖的范围里。

这给我们一个更大的启发:

构建慢时,别只盯着最后的 linker。
优化、codegen、LTO、debug info、增量构建、crate 拆分,都可能是更大的因素。

尤其 Rust 生态里,常见优化构建时间的方向包括:

  • debug build 下减少不必要的 features
  • 使用 workspace 分层
  • 避免过度泛型导致 monomorphization 爆炸
  • 调整 codegen-units
  • 合理使用 incremental
  • release 构建时慎用 fat LTO
  • CI 中缓存 target directory
  • 使用更快的 linker
  • 使用 sccache
  • 避免 build.rs 做重活
  • 减少 proc macro 开销

链接器只是其中一环。


二十七、如果你自己想复现实验,应该怎么做?

可以按这个流程来。

第一步,选一个真实项目。
不要只拿 hello world 测。hello world 没有多少链接工作,测不出真实差异。

第二步,先做 baseline。

cargo clean
cargo build --release

然后改一个小文件,再测 hot build:

cargo build --release

记录几次结果。

第三步,换链接器。

比如用 lldmold
配置方式因系统而异,但核心是确保 Rust 最终把参数传给正确的 linker driver。

第四步,验证产物。

Linux 上可以用:

readelf -p .comment target/release/your_binary

确认二进制里是否能看到对应链接器信息。

第五步,给链接器加 profiling。

mold 用:

mold -perf

lld 用:

ld.lld --time-trace --time-trace-file=...

第六步,区分完整构建时间和链接器时间。

不要看到 cargo build 5 秒,就以为链接器 5 秒。
看链接器自己的 profiling 输出。

第七步,开启或关闭 LTO 分别测。

不要把普通 release build 和 LTO release build 混在一起比较。
它们是不同工作负载。

第八步,重复多次。

构建时间会受到缓存、磁盘、CPU 频率、系统负载影响。
单次结果不可靠。


二十八、从程序员角度看:这篇文章讲的是“可观测性”

表面上,这篇文章是讲链接器。

但它更深层的主题是:构建系统的可观测性。

一个构建过程如果只是给你:

Finished release target(s) in 7.04s

那你只知道慢,不知道为什么慢。

你需要把它拆开:

Cargo 花了多久?
rustc 花了多久?
LLVM codegen 花了多久?
链接器花了多久?
链接器内部哪个阶段花了多久?
写输出文件花了多久?
LTO 花了多久?
debug info 处理花了多久?

mold -perflld --time-trace 就是在给链接器这一层增加可观测性。

这和后端服务 profiling 很像。

服务慢,你不能只看整体 RT。
你要看:

数据库查询;
RPC 调用;
序列化;
锁等待;
GC;
磁盘 IO;
网络;
CPU 热点;
缓存命中率。

构建慢也是一样。
只不过这里的组件换成了 rustc、LLVM、linker、file system、Cargo、build script。

这就是为什么这篇小文章很有价值。
它不是给出最终答案,而是教你怎么打开黑盒。


二十九、我的理解:优化构建时间不要靠玄学

很多团队优化构建时间时会有一种玄学流程:

听说 mold 很快,换 mold
听说 lld 很快,换 lld
听说 LTO 性能好,开 LTO
听说 thin LTO 折中,换 thin
听说 debug info 慢,关 debug
听说 incremental 有用,开 incremental

这些建议可能都对,但如果没有测量,很容易变成“调参拜神”。

这篇文章给出的正确姿势是:

  1. 先有 baseline
  2. 一次只改一个变量
  3. 保留命令和配置
  4. 记录多次结果
  5. 用工具看内部耗时
  6. 承认测量边界
  7. 不确定时不要强行下结论

作者在 LTO 部分的态度尤其值得学。

他没有看到奇怪数据后硬解释,而是说:

这看起来不对。要么我没有测到正确的东西,要么工作发生在别处。

这是性能分析里很珍贵的诚实。

很多时候,知道“这个数据不能支持我想说的结论”,比找到一个漂亮结论更重要。


三十、这篇文章对 Rust 开发者的实践意义

如果你是 Rust 开发者,可以从这篇文章里拿走几个实际建议。

第一,试试更快的链接器是值得的。

在 Linux 上,lldmold 都可能显著快于 GNU ld。
尤其是大型项目,链接器差异可能更明显。

第二,确认配置真的生效。

不要只改 .cargo/config.toml 就相信成功。
readelf、构建日志或其他方式确认最终产物。

第三,理解 linker driver。

如果你需要 wrapper script,GCC 的 cc 可能不接受任意 -fuse-ld 路径。
换成 clang 作为 driver 可能解决问题。

第四,区分 linker 时间和 build 时间。

如果链接器只占几百毫秒,继续纠结 mold vs lld 意义不大。
应该看 rustc、LLVM、features、proc macros、LTO 等其他部分。

第五,谨慎开启 fat LTO。

fat LTO 可能显著增加构建时间。
发布时开可以,日常开发和频繁 CI 构建不一定值得。

第六,thin LTO 是折中,但也不是免费。

thin LTO 比 fat LTO 快很多,但仍然会增加构建时间。
要结合实际性能收益判断。

第七,profiling 工具不一定覆盖你以为的范围。

lld --time-trace 看的是 lld 内部,不一定能解释 Rust/LLVM LTO 的全部耗时。


三十一、总结

《Profiling linkers》这篇文章很短,但信息密度很高。

它从一个问题开始:

为什么换了 moldlld,Rust 构建时间没有明显变快?

然后给出了一个非常工程化的回答:

不是靠猜,而是 profile。

作者先用 GNU ld 建立 baseline,发现普通 release hot build 大约 7 秒。
然后用 mold wrapper 加上 -perf,配置 clang 作为 linker driver,构建时间降到大约 5.1 秒,mold 自报链接时间约 392ms。
再用 lld --time-trace 做类似测试,构建时间约 5.2 秒,lld 自报链接时间约 449ms。
最后测试 LTO,发现 fat LTO 把构建时间拉到约 1 分 10 秒,thin LTO 则约 15 秒,但 lld trace 并没有解释这些长时间消耗,说明真正的重活可能发生在链接器 profiling 之外。

所以最终结论不是某个链接器绝对胜出,而是:

构建性能问题必须拆开看。链接器可以优化,但它不一定是最大瓶颈。LTO、LLVM、codegen 和整个构建管线可能更重要。

这篇文章也提醒我们:
性能分析最重要的不是找到一个听起来厉害的工具,而是搞清楚工具测量的边界。

如果你只看 cargo build 总耗时,你不知道链接器占多少。
如果你只看 lld --time-trace,你也不一定知道 Rust LTO 的真正成本。
只有把各层拆开,才能避免“优化了一个不是瓶颈的地方”。

对 Rust 开发者来说,这篇文章的实践价值很直接:

先测 baseline
再换 linker
确认它真的生效
再看 linker 自己的 profile
最后再决定要不要继续优化这个方向

这就是构建性能优化里最朴素、也最可靠的方法。


参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值