看完 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 #25299:Elf2,从零重写一个全新的ELF linker(目前主要针对x86_64-linux)。
这个新linker不是简单重构,而是彻底推倒重来,目标只有一个:让链接阶段在增量场景下快到“可以一直开着,不用担心拖后腿”。
源码入口主要在:
-
src/link.zig(总入口) -
src/link/Elf.zig和src/link/Elf2.zig(新版ELF实现) -
还有 src/link/MachO.zig、Coff.zig等,分别支持不同平台。
为什么这么快?源码里藏着的几个关键设计
-
为增量编译而生,从设计之初就考虑“只改一点点”
传统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无缝协作的一部分。
-
异步架构 + 现代文件系统魔法(fallocate黑科技)
在Hacker News讨论这个新linker的帖子里,大家提到它借鉴了Mold的异步思路,同时大量使用了Linux文件系统的fallocate(PUNCH_HOLE)和INSERT_RANGE。
简单说:
-
PUNCH_HOLE可以把文件中间的块“打孔”清零,而不需要把整个文件重写。 -
INSERT_RANGE则允许在文件中插入空间,而不移动后面所有数据。
这意味着:当你增量修改一个section时,linker不需要把整个输出文件从头拷贝一遍,只需局部打孔、插入、写入新数据。I/O量大幅下降,速度起飞。
Zig源码里这些操作被封装得非常干净,利用了std.fs和底层系统调用,充分压榨ext4/xfs等现代文件系统的能力。
-
垂直集成 + 零抽象开销
这是Zig整个编译栈的最大优势:frontend(Sema)→ Air IR → codegen → linker 全程用Zig自己写,没有语言切换,没有进程间通信,没有多余的序列化/反序列化。
linker可以直接拿到内存中的MIR(Machine Intermediate Representation),不用再去解析外部object文件的复杂格式(虽然也支持)。符号解析、section布局、relocation等操作都在同一个地址空间内完成,缓存友好,分支预测友好。
对比LLD:它得处理各种语言生成的object,通用性强但开销也大。Zig linker只服务于Zig自己的输出(加上少量C/汇编),可以做极致的针对性优化。
-
内存分配与数据结构极致控制
Zig一贯的风格:几乎所有内存都用ArenaAllocator或固定缓冲区管理,避免碎片和malloc开销。
在linker源码中,你会看到大量使用std.ArrayListUnmanaged、StringTable、table_section.zig等自定义结构。符号表、section header、string table的构建被设计得极度缓存局部性好,遍历时几乎不产生cache miss。
此外,新linker对debug info和strip的支持也做了优化,不会因为要生成DWARF就拖慢整个流程。
实际感受:链接快到“不存在”
现在用-fincremental + 新linker编译中大型项目时,你会发现:
-
改一行代码后,重新构建几乎感觉不到链接延迟。 -
之前很多人用 -Dno-bin(不生成二进制)来提速,现在这个flag带来的收益已经微乎其微了。
这正是Zig团队想要的效果:让开发者放心地每次构建都生成完整二进制,获得最好的调试体验和二进制一致性,而不用在速度和功能之间做取舍。
源码推荐阅读路径
想自己体会这个“魔法”,建议按以下顺序看源码(都在https://github.com/ziglang/zig):
-
src/link/Elf2.zig或当前主分支的src/link/Elf.zig—— 核心实现。 -
src/Compilation.zig中调用linker的部分,看如何集成。 -
src/link.zig—— 不同格式的抽象层。 -
关注 flush()、updateDecl()等增量相关函数。
看完你会发现:Zig的快,不是靠单一黑科技,而是几百个小优化在正确的时间点、以正确的方式叠加在一起的结果。
写在最后
Zig的linker还在快速迭代中(Mach-O、COFF等平台也在跟进),但ELF新版已经足够惊艳。它证明了一件事:当你把整个编译栈掌握在自己手里时,就能做出传统工具链难以企及的极致体验。
喜欢这篇文章的话,欢迎点赞、在看、转发给你的Zig朋友~
我们下篇见!
夜雨聆风