Zig ELF 链接器改进开发日志
- #Zig
- #ELF链接器
- #构建系统
- #增量编译
- #LLVM
开发日志
本页面包含 Zig 主分支近期变更的精选列表。也可通过 RSS 订阅。此页面包含 2026 年的条目。其他年份可在开发日志归档页面中找到。
2026 年 5 月 30 日
ELF 链接器改进
作者:Matthew Lugg
过去几周我一直在研究新的 ELF 链接器,该链接器首次亮相于 Zig 0.16.0。在 0.16.0 发布时,该链接器实现仍处于早期阶段,仅支持链接纯 Zig 代码,不涉及任何外部库(甚至不包括 libc),因此它当时(现在也是)默认禁用(可通过 -fnew-linker 启用)。不过,自首次发布以来,已经取得了相当多的进展!
这是一个很好的里程碑——根据我最新的 PR,新 ELF 链接器能够构建启用 LLVM 和 LLD 库的自托管 Zig 编译器,这需要底层许多功能的支持。
[mlugg@nebula master]$ # 使用新链接器构建 Zig 编译器:
[mlugg@nebula master]$ zig build -Dno-lib -Dnew-linker -Denable-llvm
[mlugg@nebula master]$ # 使用该编译器构建带 LLVM 和 LLD 的程序:
[mlugg@nebula master]$ ./zig-out/bin/zig build-exe ~/hello.zig -fllvm -flld
[mlugg@nebula master]$ ./hello
Hello, World!
[mlugg@nebula master]$
当然,ELF 链接器并不一定是世界上最令人兴奋的东西,因此这个新链接器的核心特性是支持快速增量编译。经过最近的增强,现在(在 x86_64 Linux 上)可以在链接外部库、C 源文件等情况下进行增量重建,且不会带来任何额外性能开销!以下是我尝试对 Andrew 的 Tetris 克隆进行测试的片段:
对 Andrew 的 Tetris 克隆进行一些愚蠢的更改,每次构建大约 30 毫秒。
哦,快速增量重建对 Zig 编译器本身也效果良好:
[mlugg@nebula master]$ zig build -Dno-lib -Denable-llvm -fincremental --watch
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 36s
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 244ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 228ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 288ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 283ms
目前这个链接器实现最大的缺失功能是尚不支持为 Zig 代码生成 DWARF 调试信息——这绝对是我接下来的优先事项。但即使没有该项支持,即时重建的便利性也令人惊叹,例如,在你进行大量打印调试的任何场景中都非常有用。
如果你正在使用 Zig 的主分支,并且使用的是 x86_64 Linux,请考虑尝试使用新的 ELF 链接器进行增量编译,如果之前它对你的项目不起作用的话!我预计许多代码库已经能够很好地配合它,从而实现毫秒级重建项目。当然,如果你遇到任何错误,请提交 issue。
如果你目前坚持使用 Zig 的发行标签版本,不用担心——正如 Andrew 在上一篇开发日志中提到的,Zig 0.17.0 即将发布,所以你很快也能尝试这个功能!
2026 年 5 月 26 日
构建系统重构
作者:Andrew Kelley
大分支刚刚合并:将执行器进程与配置器进程分离
这篇开发日志本质上是即将发布的发行说明的预览,但也是为那些希望帮助测试新功能并提供反馈以指导 Zig 项目未来发展的人提供提前通知。
之前,build.zig 文件加上构建系统实现都被编译到一个臃肿的进程中,并且是调试模式。当 build.zig 的逻辑完成在内存中构建构建图后,“构建运行器”代码执行它。
现在,build.zig 文件被编译成一个小的进程(“配置器”),以调试模式运行。当该逻辑完成在内存中构建构建图后,它会被序列化为一个二进制配置文件。父 zig build 进程知道这个文件,并将其缓存以备下次使用。在等待所有这一切的同时,它异步地编译构建图执行进程(“执行器”),采用发布模式。一旦配置文件可用且执行器进程完成编译,就会执行执行器进程,并传入配置文件。由于全局缓存,执行器进程每个 Zig 版本只需编译一次。然后执行器进程执行包含在序列化配置文件中的构建图。
此项变更的主要动机是以三种方式使 zig build 更快:
- 只有用户的
build.zig逻辑会在每次更改时被编译,而不是整个构建系统也随之编译。随着我们引入了--watch、--fuzz和--webui,这变得越来越有价值。构建系统可以增长更多功能,而不会使zig build耗时更长。 - 当构建系统知道没有任何变化时,可以完全跳过重新运行
build.zig逻辑。例如,如果你向zig build命令行添加-freference-trace,它现在会避免重复运行你的build.zig逻辑,而是使用与上次相同的配置。 - 实际执行构建图的进程现在使用优化选项编译。
为了演示第 2 点和第 3 点,下面是之前和之后运行 zig build --help 的差异:
Benchmark 1 (34 runs): master/zig build -h
measurement mean ± σ min … max outliers delta
wall_time 150ms ± 5.52ms 145ms … 165ms 4 (12%) 0%
peak_rss 84.8MB ± 275KB 84.2MB … 85.1MB 0 ( 0%) 0%
cpu_cycles 593M ± 4.01M 588M … 608M 2 ( 6%) 0%
instructions 995M ± 52.5K 995M … 995M 0 ( 0%) 0%
cache_references 25.8M ± 165K 25.4M … 26.1M 0 ( 0%) 0%
cache_misses 651K ± 20.1K 619K … 697K 0 ( 0%) 0%
branch_misses 918K ± 7.44K 906K … 935K 0 ( 0%) 0%
Benchmark 2 (348 runs): branch/zig build -h
measurement mean ± σ min … max outliers delta
wall_time 14.3ms ± 744us 13.2ms … 23.3ms 8 ( 2%) ⚡- 90.4% ± 0.4%
peak_rss 78.5MB ± 562KB 77.1MB … 81.4MB 7 ( 2%) ⚡- 7.4% ± 0.2%
cpu_cycles 24.1M ± 821K 22.8M … 27.1M 3 ( 1%) ⚡- 95.9% ± 0.1%
instructions 43.7M ± 23.8K 43.7M … 43.8M 56 (16%) ⚡- 95.6% ± 0.0%
cache_references 1.46M ± 14.6K 1.40M … 1.50M 19 ( 5%) ⚡- 94.3% ± 0.1%
cache_misses 142K ± 4.87K 127K … 157K 2 ( 1%) ⚡- 78.1% ± 0.4%
branch_misses 126K ± 1.37K 120K … 129K 12 ( 3%) ⚡- 86.3% ± 0.1%
效果非常显著,因为之前每次执行 zig build 命令都会运行 build.zig 逻辑,但现在构建系统使用缓存的序列化配置来代替。
除了性能之外,我预计像 ZLS 这样的第三方工具也会受益于使用序列化配置文件,而无需维护构建运行器的分支。
这次的变更集大幅重构了 Zig 构建系统的内部机制,但从 API 角度来看,除了上述 PR 中提到的例外情况,它基本上是非破坏性的。
对于大多数人来说,我猜测他们会遇到的主要破坏性变更如下:
if (b.args) |args| {
run_cmd.addArgs(args);
}
👇
run_cmd.addPassthruArgs();
这移除了构建脚本的一项能力,因为它们不能再观察那些参数。作为交换,更改这些参数时,构建脚本不再需要从源码重新构建。
如果你希望影响 Zig 的方向,现在是时候将你的项目升级到开发版本并尝试这些改变了。我们将在几周内发布 0.17.0。不过,如果你没有时间,并且发现 0.17.0 破坏了你的构建,别担心,我们还有机会在 0.17.1 标签中修复问题。
2026 年 4 月 8 日
使用 LLVM 进行增量编译
作者:Matthew Lugg
在上个月合并了类型解析更改后,我花了一些时间在个人项目上,不过最近我确实抽出时间对 LLVM 代码生成后端做了一些改进。这涉及到几个不同目标的多项增强,但其中一个不错的用户可见变化是我成功让增量编译在 LLVM 后端上工作了。
遗憾的是,这并不能加快可怕的“LLVM Emit Object”阶段:该时间完全取决于 LLVM。但增量编译确实有助于最小化实际 Zig 编译器代码的耗时,这意味着如果你的代码存在编译错误(因此会跳过“LLVM Emit Object”),你通常可以非常快速地得到这些错误。(当然,对于成功构建,它也确实有一定加速效果。)
这项支持现在已在主分支构建中可用,并将在 0.16.0 版本(我们很快会打标签)中包含。
对于那些还没尝试过的人,特别是如果你使用 Zig 的主分支,请务必通过向 zig build 传递 -fincremental --watch 来尝试增量编译!Zig 核心团队在近一年的工作中已经受益于增量编译,我们也从用户那里听到了好消息。该功能目前相对稳定,人们常常惊讶于只需要几毫秒而不是几秒钟就能得到最新的编译错误,从而节省大量时间。
我个人没有真正使用过 LLVM 后端进行增量编译,但现在 CI 中的所有增量测试覆盖都已为 LLVM 后端启用,并且我收到了用户的积极反馈,所以绝对值得一试。一如既往,如果你在增量编译中遇到错误,请尽可能报告它们!
谢谢,希望这对你有帮助 :)
2026 年 3 月 10 日
类型解析重新设计,附带语言调整
作者:Matthew Lugg
今天,我花费了两个月(或者可以说是三个月)的工作,合并了一个 30,000 行的 PR。这个分支的目标是重新设计 Zig 编译器内部类型解析逻辑,使其更合逻辑且更直观。对我来说,这是一个非常令人兴奋的改变,因为它让我清理了一堆编译器内部代码,并且它还带来了一些你可能感兴趣的用户可见变化!
首先,Zig 编译器现在对类型字段的分析更加懒惰:如果类型从未被初始化,那么 Zig 就无需关心该类型的“样子”。这对于那些同时作为命名空间的类型(现代 Zig 中的常见模式)非常重要。例如,当使用 std.Io.Writer 时,你不希望编译器也拉入 std.Io 中的一堆代码!下面是一个简单的例子:
const Foo = struct {
bad_field: @compileError("i am an evil field, muahaha"),
const something = 123;
};
comptime {
_ = Foo.something; // `Foo` 仅用作命名空间
}
以前,这段代码会引发编译错误。现在,它可以正常编译,因为 Zig 从未真正查看 @compileError 调用。
另一个改进是“依赖循环”体验。任何之前在 Zig 中遇到过依赖循环编译错误的人都知道,其错误消息完全无用——但现在这已经改变了!如果你遇到一个依赖循环(现在出现概率比以前略低),你将得到一条详细的错误消息,准确告诉你依赖循环来自何处。请看:
const Foo = struct { inner: Bar };
const Bar = struct { x: u32 align(@alignOf(Foo)) };
comptime { _ = @as(Foo, undefined); }
$ zig build-obj repro.zig
error: dependency loop with length 2
repro.zig:1:29: note: type 'repro.Foo' depends on type 'repro.Bar' for field declared here
const Foo = struct { inner: Bar };
^~~
repro.zig:2:44: note: type 'repro.Bar' depends on type 'repro.Foo' for alignment query here
const Bar = struct { x: u32 align(@alignOf(Foo)) };
^~~
note: eliminate any one of these dependencies to break the loop
评论