资讯详情

mold 凭什么比 ld 快 10~30 倍?把它的并行链接内核拆开看

📅 2026/10/16 12:15:48 | 华诺云谱 👁 阅读
mold 凭什么比 ld 快 10~30 倍?把它的并行链接内核拆开看
mold 凭什么比 ld 快 10~30 倍把它的并行链接内核拆开看【免费下载链接】moldmold : A Modern Linker in Rust 项目地址: https://gitcode.com/GitHub_Trending/mo/mold在 C/C/Rust 项目的构建链路里链接常常是被忽视的最后一步编译器把几百上千个.o文件编译出来之后链接器要把它们揉成一个可执行文件或共享库。对象越多、符号越杂、输出越大这一步就越慢——TensorFlow 这类项目动辄产出 9.5 GiB 的二进制用传统链接器可以卡住 50 秒以上。mold 正是冲着这个痛点来的它是 LLVM lld 原作者 Rui Ueyama 从零重写的高性能链接器宣称比 GNU ld 快 10~30 倍。本文不讨论怎么装上用起来而是直接打开它的源码看看这套加速到底建立在哪些并行化手段与数据结构之上以及10~30 倍这个数字是怎么来的。链接耗时到底花在哪传统串行链接的瓶颈根源先搞清楚链接器在干什么。以一个典型链接过程为例它要做的事大致如下读取所有输入文件解析 ELF 头、节区表和符号表符号解析把每个全局符号在多个定义中选出胜者处理强/弱定义、共享库符号、归档库按需展开布局把输入节区input section塞进输出节区output section分配虚拟地址和文件偏移处理对齐与重定位的空间需求重定位把每个引用位置按最终地址填好写输出生成节区表、符号表、动态段、.got/.plt、build-id最后落盘。传统 GNU ldbfd 后端的致命伤在于它基本是单线程、顺序执行的。符号解析要对每个符号做字符串比较和哈希查找几十万甚至上百万个符号逐个处理归档库展开拉一个成员进来是串行决策重定位与写输出同样只有一个线程在搬字节。当输出膨胀到数 GiB、输入文件上万时这些看似不起眼的 O(n) 步骤全部串起来就变成了以十秒为单位的等待。更隐蔽的浪费在 I/O 与内存侧。链接器要把输入文件的字节读进内存、把输出按顺序写出单线程的读写和页错误page fault完全无法被多核隐藏。mold 作者在 C 版本时代就总结过链接的性能天花板从来不是 CPU 单核算力而是有多少工作可以并行、有多少数据竞争可以被数据结构消掉。mold 的并行化与算法数据结构如何打破瓶颈mold 的提速不是某个 pass 优化了一下而是从文件读取到最终落盘的全链路并行 为并发重新设计的数据结构。打开仓库源码可以从 elf/driver.rs 的link()主流程一路看到这些手段。1. 输入文件并行读取I/O 与解析都不再排队大型程序的输入文件数以万计mold 在 elf/reader.rs 里直接并行打开、mmap 并解析所有输入文件连归档库成员都每个成员一个任务地并行展开。代码注释写得很直白Reading is I/O- and CPU-intensive and a large program has tens of thousands of input files, so files are read in parallel.并行解析面临一个难题符号表是全局共享的多个线程同时往里插符号必然竞争。mold 的解法是WorkerLocal 分桶——elf/context.rs 里用symbol_bins: OnceLockWorkerLocalBinsSymbolSlot给每个 worker 一个独立的符号桶解析时各自往自己的桶里记最后再合并归位gather_symbols。这样把争一把大锁变成了每线程写自己的一块竞争被结构性消除。命令行的顺序语义如--as-needed只作用于其后文件则在所有文件读完后按命令行位置排序回原始顺序来还原见 elf/reader.rs 的read_input_files()。2. 无锁并发哈希表链接器的心跳符号去重、字符串 intern、mergeable 节区碎片去重……几乎每一步都在用哈希表。mold 没有借用现成的并发容器而是自己实现了一个理由写在 common/concurrent_map.rs 的注释里Weve implemented this ourselves because the performance of a concurrent hash map is critical for our linker.这张表的关键设计有三点无锁插入线程用compare-and-swap抢占空桶CLAIMED哨兵标记已抢占未发布其余线程自旋等待 key 发布全程没有锁按 shard 探测表分成 64 个 shardNUM_SHARDS: usize 64探测序列只在自己的 shard 内绕圈不同 shard 的条目可以独立布局、互不干扰mmap 大页内存用mmap一次性分配并madvise_hugepage减少 TLB 缺失和缺页开销。配合 common/siphash.rs 的自研哈希和 common/hyperloglog.rs 的唯一值基数估计用 HLL 预估符号总量从而给表一次性分配合适容量——这张表满了就直接中止所以容量估计必须准插入路径上连扩容都省了。3. 符号解析的并行与原子化符号解析本来是最难并行的环节每个符号的胜者由强/弱定义、是否来自共享库/归档库、命令行优先级共同决定。mold 在 elf/input_files.rs 中把优先级编码成一个可比较的 ranksymbol_resolution_rank/resolved_symbol_rank让每个对象文件并行调用resolve_symbols()竞争每个符号的归属。竞争本身落在符号条目上的原子状态字节上注释里还专门解释了为什么先做 relaxed load 再走原子 RMWA relaxed load branch (assuming miss) takes only around 20 cycles, while an atomic RMW can easily take hundreds on x86.同样的原子化思路遍布全仓SymbolId是 u32 句柄Origin用 64 位打包了 4 种来源类型节区/输出块/碎片/符号以省内存可参考 elf/symbol.rs。4. 后台任务与流水线让核永远不闲着链接各 pass 之间存在依赖但并非全程都是串行依赖。mold 在 common/parallel.rs 里提供了一个BackgroundT后台任务原语把结果稍后才需要的计算提前 spawn 到 Rayon 线程池里跑主线程继续推进后续 pass需要时再join()。在 elf/driver.rs 中.gdb_index的输入读取、建表都被拆成后台任务与前台布局、复制工作重叠执行——甚至建表任务还特意限制并发度(threads * 3 / 8).clamp(1, 12)避免和前台工作抢核。整条link()流程就是一张按依赖排序的 pass 流水线每个 pass 内部再用par_iter并行。5. 写输出并行搬字节 大页 mmap最后的大头是把所有节区内容拷进输出文件并就地应用重定位。在 elf/driver.rs 的copy_chunks()/run_tasks()里输出缓冲区被按节区拆成互不相交的字节区间每个 chunk 一个任务into_par_iter().for_each(...)并行写。有特殊写入依赖的如.symtab要连带写.strtab、.eh_frame要填.eh_frame_hdr表通过Task.extra显式声明并调度到同一任务内保证各写各的区间这一不变式。输出侧还有一个容易被忽略的优化common/output_file.rs 用mmap映射输出文件并madvise_hugepage注释明确给出收益Linking a Chromium debug build is ~20% faster with this madvise call.原因很具体没有大页时输出文件的每个 4 KiB 页首次写入都要触发一次缺页而多个拷贝线程的缺页会在文件页缓存上串行化用大页large folio后缺页次数下降一个数量级。同文件里还针对 ext4 做了fallocate预分配在 tmpfs 上则跳过因为内核会单线程清零整个区间反而更慢。这类抠细节的注释在仓库里到处都是——比如 common/parallel.rs 的stable_partition把数组按 16384 大小的块并行分区common/jobs.rs 提供MOLD_JOBS1全局锁防止多进程链接挤爆内存。6. 线程数是克制的值得一提的反直觉设计mold 把线程数上限设成 32elf/driver.rs 的thread_count()注释说mold 在太多线程时并不会继续变快。这说明它的并行度不是靠无限堆线程而是靠每个线程干的有效工作——这是它与看起来并行的玩具级实现的分水岭。ld 1/10~1/30 的加速比是怎么测出来的10~30 倍不是拍脑袋。mold 官方在论文mold: A Massively Parallel Linker以及历史基准中给出的典型数字是相对 GNU ldbfd链接时间可缩短到原来的 1/10~1/30这一点也被大量社区文章反复引用。当前仓库 README.md 里挂着一份 2026 年 8 月的最新基准口径更严格对比对象是 LLVM lld 和新兴的 wild 链接器全部以 release 配置从源码构建、使用默认参数每项取预热后的三次运行中位数跑在 AMD Ryzen Threadripper 7980X64 核和 Apple M1 Ultra 两台机器上且覆盖 debug 与 release 两种构建。摘录 Threadripper 上的 debug 构建结果程序输出大小lldwildmoldlld/moldChromium 1454.51 GiB16.64s3.98s1.65s10.1xChromium 145 ARM644.76 GiB20.91sN/A1.83s11.4xClickHouse 26.15.58 GiB6.62s4.40s0.98s6.7xTensorFlow 2.219.55 GiB50.73sN/A3.15s16.1xGodot 4.61.01 GiB1.77s1.08s0.46s3.8x完整表格在 README.md 的 Benchmark 一节九个大型程序在 Threadripper 上相对 lld 的中位数加速比是4.9x最大单例是 TensorFlow 的 16.1x。注意这里对照的是已经很快的 lld若对照单线程的 GNU ld差距还会大得多——这正是10~30 倍说法的来源。这份基准同样诚实地揭示了加速比的边界值得任何想引用的读者注意核数决定上限M1 Ultra16 性能核上同样程序相对 lld 的中位数约为 2.5x 左右Chromium 仍能到 4.3~5.5x但 Clang 只有 1.5xrelease 构建差距缩小release 输出小一个数量级链接本身耗时缩短绝对优势依然存在如 Chromium release 仍快 10.2x、TensorFlow 快 13.7x但部分小程序的倍数会降到 2~5x 区间越大的输出倍数越夸张9.55 GiB 的 TensorFlow debug 版从 50.73s 压到 3.15s这是最贴近10~30 倍口号的场景——因为拷贝字节、缺页、重定位这类开销在大输出里占比最高恰好都是 mold 并行化收益最大的部分。结语把 mold 的源码从头到尾翻一遍你会发现它没有魔法并行读文件、无锁哈希表、原子符号解析、后台任务重叠、大页 mmap、按区间并行写输出——每一项都是针对链接的真实瓶颈做的工程决策而这些决策的密度才是它把 ld 甩开一两个数量级的原因。如今这个仓库已经演进为 3.0.0 版本的 Rust 实现见 Cargo.tomledition 2024、MIT 许可继续保持着链接是构建的最后一公里这一朴素而有力的叙事当编译早已吃满多核时链接不该再是那个让整个团队等着的单线程瓶颈。【免费下载链接】moldmold : A Modern Linker in Rust 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑