乐于分享
好东西不私藏

看完 Zig linker 源码才明白它为什么这么快(第19篇)

看完 Zig linker 源码才明白它为什么这么快(第19篇)

大家好,我是Zachel,欢迎来到Zig源码学习系列第19篇。

编译速度一直是Zig最硬核的卖点之一。别人还在等LLVM慢慢吐出二进制,Zig已经“嗖”的一声跑完了整个构建流程。很多人以为这只是因为“自托管编译器”或者“绕开了LLVM”,但真正决定最终二进制生成速度的,其实是linker(链接器)。

今天,我们就钻进Zig的源码,看看它的自托管linker到底做了哪些“黑魔法”,让链接阶段快到离谱,尤其在增量编译场景下,几乎感觉不到链接的存在。

先说清楚:Zig的linker演进史

早期Zig依赖LLVM自带的LLD(lld linker)。LLD已经很快了,但它是为通用场景设计的,并没有针对Zig的编译模型做深度优化。特别是增量编译(incremental compilation)这个Zig的核心目标,LLD几乎没有原生支持——每次改一行代码,它还是得把几乎所有东西重新走一遍。

后来Zig团队决定:自己写!

  • 先有src/link/Elf.zig等老版本自托管linker。
  • 2025年重磅PR #25299Elf2,从零重写一个全新的ELF linker(目前主要针对x86_64-linux)。

这个新linker不是简单重构,而是彻底推倒重来,目标只有一个:让链接阶段在增量场景下快到“可以一直开着,不用担心拖后腿”。

源码入口主要在:

  • src/link.zig(总入口)
  • src/link/Elf.zig 和 src/link/Elf2.zig(新版ELF实现)
  • 还有src/link/MachO.zigCoff.zig等,分别支持不同平台。

为什么这么快?源码里藏着的几个关键设计

  1. 为增量编译而生,从设计之初就考虑“只改一点点”

传统linker每次链接都是“从零开始”:读所有object文件 → 解析符号 → 布局section → 写文件。

Zig的新linker则深度集成Zig的编译缓存和依赖追踪系统。它能精确知道“这次改动只影响了哪些section、哪些符号”,只处理dirty的部分。

在PR的benchmark里:

  • 构建Zig编译器本身,改一行代码后:
    • 老linker:增量链接约191~194ms
    • 新Elf2:只需64~65ms
    • 甚至接近完全跳过codegen+linking的62ms!

也就是说,链接只比“不链接”慢了4%左右。这在以前是不可想象的。

源码层面,这种增量能力靠的是精细的脏标记(dirty tracking)和缓存友好的数据结构。linker不再是编译流程的“瓶颈末端”,而是和codegen、Sema无缝协作的一部分。

  1. 异步架构 + 现代文件系统魔法(fallocate黑科技)

在Hacker News讨论这个新linker的帖子里,大家提到它借鉴了Mold的异步思路,同时大量使用了Linux文件系统的fallocate(PUNCH_HOLE)INSERT_RANGE

简单说:

  • PUNCH_HOLE 可以把文件中间的块“打孔”清零,而不需要把整个文件重写。
  • INSERT_RANGE 则允许在文件中插入空间,而不移动后面所有数据。

这意味着:当你增量修改一个section时,linker不需要把整个输出文件从头拷贝一遍,只需局部打孔、插入、写入新数据。I/O量大幅下降,速度起飞。

Zig源码里这些操作被封装得非常干净,利用了std.fs和底层系统调用,充分压榨ext4/xfs等现代文件系统的能力。

  1. 垂直集成 + 零抽象开销

这是Zig整个编译栈的最大优势:frontend(Sema)→ Air IR → codegen → linker 全程用Zig自己写,没有语言切换,没有进程间通信,没有多余的序列化/反序列化。

linker可以直接拿到内存中的MIR(Machine Intermediate Representation),不用再去解析外部object文件的复杂格式(虽然也支持)。符号解析、section布局、relocation等操作都在同一个地址空间内完成,缓存友好,分支预测友好。

对比LLD:它得处理各种语言生成的object,通用性强但开销也大。Zig linker只服务于Zig自己的输出(加上少量C/汇编),可以做极致的针对性优化。

  1. 内存分配与数据结构极致控制

Zig一贯的风格:几乎所有内存都用ArenaAllocator或固定缓冲区管理,避免碎片和malloc开销。

在linker源码中,你会看到大量使用std.ArrayListUnmanagedStringTabletable_section.zig等自定义结构。符号表、section header、string table的构建被设计得极度缓存局部性好,遍历时几乎不产生cache miss。

此外,新linker对debug info和strip的支持也做了优化,不会因为要生成DWARF就拖慢整个流程。

实际感受:链接快到“不存在”

现在用-fincremental + 新linker编译中大型项目时,你会发现:

  • 改一行代码后,重新构建几乎感觉不到链接延迟。
  • 之前很多人用-Dno-bin(不生成二进制)来提速,现在这个flag带来的收益已经微乎其微了。

这正是Zig团队想要的效果:让开发者放心地每次构建都生成完整二进制,获得最好的调试体验和二进制一致性,而不用在速度和功能之间做取舍。

源码推荐阅读路径

想自己体会这个“魔法”,建议按以下顺序看源码(都在https://github.com/ziglang/zig):

  1. src/link/Elf2.zig 或当前主分支的src/link/Elf.zig —— 核心实现。
  2. src/Compilation.zig 中调用linker的部分,看如何集成。
  3. src/link.zig —— 不同格式的抽象层。
  4. 关注flush()updateDecl()等增量相关函数。

看完你会发现:Zig的快,不是靠单一黑科技,而是几百个小优化在正确的时间点、以正确的方式叠加在一起的结果。

写在最后

Zig的linker还在快速迭代中(Mach-O、COFF等平台也在跟进),但ELF新版已经足够惊艳。它证明了一件事:当你把整个编译栈掌握在自己手里时,就能做出传统工具链难以企及的极致体验。

喜欢这篇文章的话,欢迎点赞、在看、转发给你的Zig朋友~

我们下篇见!