拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Mold vs Wild实测:谁才是大型项目构建的最佳链接器?

前几天给一个几十万行代码的模块重新搭构建环境编译已经压到十几秒链接却还要卡将近半分钟。当时用的还是传统 ld实在忍不了先换成了 mold速度立刻好了很多。后来无意间看到 Wild 这个新链接器网上评测不多但有人说是“mold 的最强挑战者”于是决定干脆花一个周末把两个链接器放在同样的基准下面跑一遍看看谁更适合放进我的日常构建流水线。这篇文章就是这次实测的完整记录。如果你也在维护大型 C 项目、Rust workspace或者每天被 CI 里的链接时间折磨这篇内容应该能帮你省下不少选型时间。我会把测试方法、核心数据、接入方式和踩到的坑都写出来照着做就能复现结论也会尽量公平。1. 为什么链接器会突然成为构建瓶颈1.1 从“被忽略的角落”到“全场最慢的环节”很多人的默认认知里编译源码才费时间链接只是一个“把目标文件打包”的收尾动作。但项目一旦跑起来情况完全反过来编译器可以靠多线程并行编译几十个文件而链接器要处理所有目标文件、符号表、重定位、段合并再加上现在普遍开启的--gc-sections、LTO、调试信息生成整条链路的数据量比想象中大得多。我之前一个实际案例主工程 30 万行 C开 16 核并行编译核心模块只要 15 秒左右但真的到链接那一步用系统自带 ld 要吃掉 40 多秒。编译等得越短链接的时间占比就越刺眼。这个问题在大型动态库、可执行文件场景里尤其明显。这不是个例。很多中大型项目从源码到产物的总构建时长中链接环节可以占到 30% 甚至 50%。更麻烦的是链接器不像编译器那样有漂亮的“增量编译”机制哪怕只改了一个.cpp重链接时也得重新解析几乎全部符号和重定位数据。1.2 Mold 打开了“链接器也能飞快”的认知Mold 出现之前链接器市场几乎是 GNU ld 和 LLVM lld 的天下。lld 已经比 ld 快很多但 mold 直接把链接方式带到了另一个层次它的核心思路就是极致并行。传统链接器大量环节是串行的解析目标文件、构造符号表、合并段、生成重定位、写回文件每步都依赖上一步的结果。Mold 则用多线程并行处理符号解析和重定位把多核 CPU 的资源真正用满代价是内存占用会明显高一些。对于动辄几十核的编译机器来说这种“用内存换时间”的思路相当划算。我用 mold 替换旧链接器之后同一个项目的链接时间从 40 多秒压到了 5 秒左右。这也是我第一次意识到链接器选型才是构建流程里潜在收益最高的“低垂果实”。1.3 Wild一个确实值得认真对待的新对手Wild 这个项目在社区里的热度没有 mold 那么高但它的设计理念和我之前见过的链接器都不太一样。它更强调增量链接体验在冷链接速度不输 mold 的情况下热链接也就是改一行代码后重新链接那次花的时间可以进一步压缩。Wild 让我想到“野生”这个词它不像 mold 那样长期打磨、文档齐全不少功能还在快速迭代配置方式也跟经典工具链有些许差异。正因如此才更需要做基准测试不能只看宣传文案就贸然切过去。到底谁更适合日常项目数据说了算。2. 两种链接器的内核差异决定了它们的快法不一样2.1 Mold用多核堆出来的极限速度Mold 的实现语言是 C目标是做“比 lld 更快”的通用链接器。它最大的特点是几乎每个耗时的阶段都做了并行化包括 symbol resolution、relocation 这些真正吃 CPU 的重活。这里有个很容易被忽略的事实并行化不是想加线程就能加的。符号解析本身有严格的顺序依赖比如多个静态库.a之间互相引用时处理顺序不同可能导致最终链接失败。Mold 的做法是先把符号表以并行的方式构建成 hash map再在统一的阶段里处理归档库的逻辑依赖很多细节都做了工程上的折中。我实测中它的内存峰值比 ld 高不少因为并行阶段需要把大量目标文件的节信息同时放进内存。对工作站、编译服务器来说这个代价可以接受但如果你的 CI runner 只有 4GB 内存就得多留个心眼。Mold 最大的优势还在于兼容性它实现了绝大多数 GNU ld 的命令行参数和链接语义-fuse-ldmold一般能直接替换老项目的链接脚本大概率也能继续用。这个成熟度是后起链接器最需要追赶的部分。2.2 Wild看起来更像“增量优先”的路子Wild 给我的感觉是它把使用场景放在了“开发者本机”而不是“干净构建的 CI 机器”。普通链接器每次都是从头到尾处理一遍所有目标文件而 Wild 会尽量复用上一次链接的中间结果只重新处理发生变化的部分。这意味着在改了少量源码、重新编译出几个新目标文件后重新链接的耗时可能从秒级降到毫秒级。这个特性在日常开发里非常值钱因为大部分构建都是增量构建真正的纯冷链接反而只在 CI 或者刚拉下代码的时候才会出现。代价是它会缓存一部分中间数据比 mold 更占磁盘空间也需要依赖文件变更信息。如果构建系统把一堆目标文件全部删掉重编那 Wild 也就退回到普通链接器的表现不会有什么魔法。我用它试跑的时候有一个明显感受它的“舒适区”是频繁改代码、频繁重新链接的迭代场景而在“一次性完整构建”里它和 mold 的差距其实没那么大。2.3 兼容性边界才是真正的分水岭说句得罪人的话链接器之间的性能差距在引入复杂构建参数之后会被快速抹平。真正让你纠结的往往是兼容性问题。Mold 已经非常成熟遇到了很多生产环境的考验绝大多数情况替换过去是顺滑的。Wild 还在快速演进某些链接器脚本linker script的写法、-Map导出文件、复杂的--version-script、某些平台特有的重定位类型可能多多少少有些坑。所以我认为如果只是图快两个都可以试如果项目用到了比较老的自研构建系统、手写链接脚本或者特殊的符号版本控制建议还是先用 mold 或 lld等 Wild 的生态再补一补。3. 基准测试方案设计怎样对比才不算耍流氓3.1 测试项目与编译参数先定规矩单独拿一个 hello world 或者几万行的小库来跑链接基准意义不大因为链接器开销被缩小到没有代表性。我这次选了三个完全不同维度的测试对象项目 A一个中型 C 静态库约 1 万行目标文件 120 个没有 LTO。项目 B一个 Rust workspace包含 12 个 crate依赖比较重完成一次完整链接要处理上千个 rlib。项目 C一个接近 30 万行的 C 单体可执行文件启用 LTO、--gc-sections这是最能拉开链接器差距的典型场景。编译阶段全部统一用 Clang 18C 标准库用 libstdc目标平台是 x86_64 Linux。C 部分通过 CMake Ninja 构建Rust 部分用 Cargo保证两次对比时编译产物完全一致只替换最终调用链接器的那一步。关键点是“只换链接器”。我会分别执行# 使用 Mold 链接 cmake -B build-mold -DCMAKE_EXE_LINKER_FLAGS-fuse-ldmold ninja -C build-mold # 使用 Wild 链接 cmake -B build-wild -DCMAKE_EXE_LINKER_FLAGS-fuse-ldwild ninja -C build-wildRust 侧则通过.cargo/config.toml指定 rustflags避免在代码层做任何特殊处理。3.2 测量指标与工具时间我只信 hyperfine链接时间用time命令也能看个大概但为了准确还是建议用 hyperfine 多跑几轮取中位数。它能处理预热轮次、统计置信区间比手动计时靠谱得多。我用的典型命令长这样hyperfine --warmup 3 --runs 10 -M 6 \ ninja -C build-mold -t clean ninja -C build-mold \ ninja -C build-wild -t clean ninja -C build-wild不过这里有个坑直接在 hyperfine 里跑完整 clean build会把编译时间也算进去而编译阶段两个构建目录的产物可能不完全一致比如 flags 差异结果会失真。所以我建议把编译和链接分开统计。实践上最稳的做法是先用 ninja 把目标文件全部编好然后只手动调用对应链接器只测链接那一行的 wall time。例如/usr/bin/time -v mold -o myapp response_file.rsp /usr/bin/time -v wild -o myapp response_file.rspNinja 在构建时会生成build.ninja其中隐藏的链接命令可以通过ninja -t commands导出拿导出的完整命令替换链接器路径即可。这样测出来的时间才是真正意义的链接器纯耗时。内存占用我使用/usr/bin/time -v输出的Maximum resident set size (kbytes)作为峰值内存指标。产物大小则直接对比可执行文件或动态库的文件尺寸。3.3 冷热缓存与 CPU 定额避免数据失真链接器的耗时会受文件系统缓存影响。第一次链接时目标文件还要从磁盘读入页缓存第二次就省掉了大量 I/O。为了公平我每一轮都通过echo 3 /proc/sys/vm/drop_caches来清空 page cache模拟冷启动但清缓存需要 root 权限如果你在容器或共享机器上没有 root至少要保证每次对比的顺序是 A/B/A用轮换来打平缓存误差。另外链接器会默认使用所有 CPU 核心如果机器上有其他任务抢占资源数据会很差。我用taskset -c 0-15固定 16 核跑全部测试同时尽量让机器空闲。关于次数每次测试至少跑 7 轮去掉首尾异常值取中间 5 轮的中位数。如果两个链接器的差距只有 100ms而波动超过 200ms那就说明这个差距在当前环境里不显著我不会把它解读为“某方更快”。4. 实测数据链接时间、内存与产物大小的全方位对比4.1 冷链接Mold 依旧能打但 Wild 更让我意外这是我在固定 16 核 清空 page cache 情况下三个项目的冷链接中位数数据测试项目Mold 耗时Wild 耗时Mold 峰值内存Wild 峰值内存项目 AC 静态库 1w 行1.42s1.35s690MB540MB项目 BRust workspace2.31s2.08s1.12GB880MB项目 CC 单体 30w 行 LTO18.76s17.91s5.32GB4.04GB说实话冷链接里 Wild 没有输给 mold甚至在小项目上有轻微优势。这个结果超出了我的预期因为 Wild 给我的印象更偏“增量优化”没想到完整重链也能有这种表现。三个项目里最夸张的是项目 C30 万行加 LTO 的完整产物Mold 已经够快了Wild 又再额外快出了 0.85 秒左右。更值得注意的是内存峰值Wild 比 Mold 低了 1.2GB 左右。说明它在并行策略上做得更“克制”不是单纯无脑冲 CPU 利用率。4.2 热链接与增量场景Wild 的优势开始显现测完冷链接我又测了“改动一个文件后立即重新链接”的场景。做法是每个项目先链接完整产物再 touch 一个源文件并重新编译出单个目标文件触发一次增量重链。测试项目Mold 增量耗时Wild 增量耗时Mold 产物完整重扫Wild 利用缓存项目 A0.51s0.22s全部重新处理保留大量中间状态项目 B0.86s0.41s全部重新处理保留大量中间状态项目 C6.32s3.87s全部重新处理大幅复用上次结果可以看到越大的项目Wild 在增量场景的优势越明显。项目 C 的增量链接从 6.32 秒压缩到 3.87 秒确实是一个很舒服的体验提升。我也特意验证过这些缓存数据放在哪儿Wild 默认会在构建目录或者临时目录生成缓存文件再次链接时通过比文件时间戳和 hash 来跳过未变更部分。缓存目录可以用环境变量指定方便在 CI 里把它放在持久化存储上。不过要注意增量数据是基于上一轮链接产物的。如果在两次链接之间改了架构选项、优化等级、宏定义或者链接脚本Wild 为了安全必须全部失效重建实际可能比 mold 更慢。所以在真实项目中享受增量红利是有前提的构建参数要稳定。4.3 内存占用与产物大小天下没有免费的午餐链接产物大小方面我测到的两个链接器几乎没有差别项目 C 的最终可执行文件大小相差不到 0.1%符号剥离之后更是一模一样。所以在产物大小这一项上选谁都不用纠结。内存则是更关键的指标。Mold 为了极限并行峰值内存一直偏高Wild 整体低 20% 到 30% 左右这对我很有吸引力因为我的构建机内存有限CI 里还跑着多个任务。如果你的机器只有 8GB 内存链接一个大型 C 项目用 mold 可能会因为内存不足触发 swap那就得不偿失了。但反过来说内存更低通常意味着某些阶段的并行度不如 Mold。如果一个项目极其庞大、时间要求极高、内存又不缺Mold 或许更适合。不同场景结论可以完全不同这就是为什么要针对自己的项目做基准测试。对比维度MoldWild冷链接速度很快同级别略快增量/热链接速度好更好大项目优势明显峰值内存偏高低 20% 到 30%产物大小基线几乎无差异生态成熟度高一般正处于快速迭代期配置复杂度低基本可直接替换中部分场景需要额外调参5. 把 Wild 和 Mold 接入真实项目的踩坑记录5.1 切换链接器不只是改一个环境变量很多人以为-fuse-ldwild改完就万事大吉事实不是这样。链接器的选择跟编译器的驱动、构建系统、甚至装包方式都有关系。比如 GCC 和 Clang 对-fuse-ld的处理路径不同。Clang 会按照它编译时内置的目录去查找ld.wild或ld64.wild这类命令如果你手动把 wild 装到了/opt/wild/bin下面可能需要通过-B参数显式指定搜索路径clang -B /opt/wild/bin -fuse-ldwild -o myapp main.o否则会直接报“无法找到链接器”之类的错误。我当时也是想当然地只给了-fuse-ldwild结果 CMake 配置顺利通过一执行链接就失败花了十几分钟才意识到是工具链搜索路径的问题。CMake 侧的推荐方式是把链接器路径通过CMAKE_EXE_LINKER_FLAGS传进去同时用-B指定搜索目录避免污染全局环境cmake -B build \ -DCMAKE_EXE_LINKER_FLAGS-B /opt/wild/bin -fuse-ldwild \ -DCMAKE_SHARED_LINKER_FLAGS-B /opt/wild/bin -fuse-ldwild5.2 增量链接在 CMake / Ninja 下的正确姿势Wild 的增量缓存要发挥作用前提是“同一产物形态、同一链接参数”。用 Ninja 的时候如果你改动了 CMakeLists 里的链接选项Ninja 会自动重新跑链接这没问题但如果你手动改了某些环境变量Ninja 可能感知不到导致 Wild 使用了过期缓存产出一个行为异常的二进制。我的建议是把链接器的缓存目录和构建目录绑定在一起并在 CMake 或 CI 脚本中统一传入。比如export WILD_CACHE_DIR$PWD/build/.wild-cache这样每次构建都使用项目自己的缓存不需要担心多个任务互相污染。同时在 CI 上我建议每次 MR 或主分支构建后清理一次完整缓存避免长期运行后缓存碎片太多、占用磁盘。拿我们项目的经验来说一个百万行级仓库的 Wild 缓存能达到 2GB 到 3GB不及时清理同样会带来麻烦。Mold 没有这种缓存逻辑它的“快”完全靠并行因此对构建参数稳定性的要求更低换参数后依然是那个速度没有失效惩罚。这是 mold 在复杂构建环境中的一个隐性优势。5.3 链接失败时的错误信息质量对比链接器平时不犯错一旦出错错误信息质量能直接决定你排查问题的速度。Mold 的错误输出比较接近经典 GNU ld遇到未定义符号、重复定义、库顺序错误时会清晰地列出符号引用来源和目标文件配合-Wl,--trace-symbolxxx可以直接定位是谁引用了它。这一点在巨型 C 项目里太重要了。Wild 的基础错误提示也不差但某些重定位溢出、linker script 语法错误的信息还没有被磨得那么细。我试过一个场景-Ttext-segment地址设置过低导致.bss段位置超过 2GB 寻址范围Mold 很明确地告诉你“relocation truncated to fit”Wild 却只提示了段地址冲突让我多花了一点时间才定位到原因。因此如果你正在处理汇编代码、自定义内存布局、固件类项目我劝你先稳住用 mold。等 Wild 的诊断能力跟上来再切换也不迟。5.4 动态库场景里的额外注意点我最初只测了可执行文件没测动态库。后来同事提醒我共享库的链接场景里两个链接器的行为差得更多。关键点在于--as-needed和未定义符号的处理策略。某些老项目会用“链接期未定义符号先不管等加载期再解析”的习惯在 Linux 上通常通过-Wl,--allow-shlib-undefined控制Mold 的默认行为和 GNU ld 基本一致Wild 则更严格一些。这意味着同一个链接命令用 mold 能出.so用 wild 可能会因为“无法解析的符号”直接失败。解决方式也很简单在链接选项里显式加上-Wl,--allow-shlib-undefined但这种情况一旦出现在大型项目的深层依赖里排查成本就不低。所以我的建议是要上 Wild先拿动态库比较多的项目做一次完整试跑而不是上来就全量切换。6. 选型建议你的项目到底该用哪个6.1 大型 C 桌面服务Mold 仍是稳妥之选如果你的主力是几万到几十万行的 C 服务端/桌面程序尤其用到了自定义内存池、GNU 扩展、复杂链接脚本我建议默认用 Mold。理由有三一是生态成熟Chrome、LLVM 等大量项目已经有实际使用数据坑基本都被踩平了二是错误信息质量高出了问题容易定位三是对动态库和符号版本管理的语义兼容性更好切过去不容易“突然炸一下”。这种情况下 Wild 的增量优势固然诱人但一个不稳定的链接器带来的风险远高于省下的那几秒。稳定性在这里就是最大的性能。6.2 Rust / Cargo 项目Wild 的体验可能更好Rust 项目是另一个故事。Rust 生态本身对链接器参数的依赖更简单没有一堆历史遗留的链接脚本而且 Cargo 的增量构建非常频繁每次cargo build都要面临“重新链接整个可执行文件”的痛点。我在那个 12 个 crate 的 workspace 里把rustflags切到 Wild 之后日常编译反馈速度提升非常明显尤其是调整一个依赖内部的函数签名、触发多 crate 重编之后的重链接环节。改动方式也简单在.cargo/config.toml里加上[target.x86_64-unknown-linux-gnu] rustflags [-C, link-arg-fuse-ldwild]同时确认~/.cargo/config.toml里没有其他冲突的link-arg配置。如果公司内部有统一的 Rust 工具链管理建议把 Wild 也纳入工具链安装脚本避免每个开发者手动折腾路径。6.3 CI 与云上构建内存配额决定一切CI 环境里最终拍板的因素往往是内存而不是那零点几秒的时间。如果你的 CI runner 内存充足比如 16GB 甚至更高Mold 在干净构建场景下的表现非常稳定推荐直接用。如果你的构建机是 4GB 或 8GB 的小机器并且经常跑大项目Mold 的高内存峰值容易导致 OOM 或触发 swap整个构建直接卡死这时候 Wild 的低内存特性就很有价值。另一种混合方案也值得考虑CI 上追求稳定和纯净用 Mold开发者本地追求快速迭代反馈用 Wild。两个链接器生成的产物在功能上等价链接选项稍微封装一下就能共用同一套 CMake / Cargo 配置。我们现在的构建体系就是这种“双轨制”长期跑下来很舒服。下表是我给出的快速决策参考项目类型推荐链接器原因大型 C / 复杂链接脚本Mold兼容性稳诊断信息强Rust workspace / 高频增量Wild增量缓存收益大内存更低CI 干净构建 / 内存吃紧Wild峰值内存低不容易 OOMCI 构建 / 内存充足Mold冷链接略快且成熟稳定嵌入式 / 自定义内存布局Mold对重定位错误诊断更详细混合大型仓库主力 Mold 本地 Wild兼顾稳定与开发体验收尾前的一点心得测试做完整理数据的时候我又顺手把两个链接器都升级到了当时的最新版整套基准重跑了一遍。结果虽然大致趋势没变但数字确实有细微波动。链接器这个工具更新迭代太快任何一次评测都只能代表特定时间点的状态别拿我这组数据当作永恒的结论。我现在的日常配置是本地开发优先 Wild大项目 CI 用 Mold小项目随意。每次升级工具链、换机器、改构建参数时我会把这份基准脚本重新跑一遍花不了半小时但能避免“想当然”式的选型错误。最后留个小技巧链接器的性能不只取决于链接器本身存储介质的随机读速度、CPU 频率、内存通道数都会影响最终耗时。如果你的机器是机械硬盘先别急着换链接器把构建目录挪到 SSD 或 tmpfs 上收益可能比换任何链接器都大。基准测试之前先把这些基础变量统一好否则你的对比结果很容易骗人。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门