一个像素编辑器,改一行代码,保存,重新编译。50到70毫秒后,程序就跑起来了。

这是 Zig 核心团队成员 mlugg 放出的实测:项目叫 Fizzy,一个真实、复杂的应用,不是凑数的玩具例子。首次完整构建大约 5 秒,之后每一次改动的增量重编译,稳定落在 50 到 70 毫秒之间。这个数字值得写,不是因为它快,而是因为它是 Zig 从语言设计阶段就开始铺垫的结果,不是某一次孤立的缓存优化。

发生了什么,谁现在能用上

先说清楚一个前提:这套完整体验目前只在 master 分支跑得动。

版本增量编译链接能力现在能用吗
0.16.0(稳定版)支持缺关键能力不完整
master(开发版)支持齐备完整,但要自己编译工具链
0.17.0(未发布)预期支持预期齐备官方没给具体日期

这意味着两类人现在的选择不一样。愿意折腾、自己编译master、能接受不稳定的开发者,现在就能试到70毫秒级的重编译体验。做正式项目、追求稳定构建流程的团队,现实一点的做法是等0.17.0,现在切master可能因为链接能力不全反而卡住,得不偿失。

Fizzy 实测(master 分支,单机单次) ~5s 首次完整构建 50-70ms 改动后增量重编译 数据来自单个项目单台机器演示,非通用基准

这也是一次演示,一个项目、一台机器。不是所有 Zig 项目改一行都能稳定拿到70毫秒,大型代码库、复杂comptime逻辑的实际表现,目前还没有公开的横向数据。

拆解:编译器怎么知道该重算哪一段

第一步是把源文件解析成 ZIR,一种中间表示。这一步是纯函数,同一份源码永远转出同一份 ZIR。Zig 早就把这部分按文件缓存到磁盘,文件没改就直接读缓存,几乎不耗时间。这部分不难,很多编译器都做得到。

真正的硬骨头是语义分析:类型检查和 comptime 求值。Zig 把代码拆成更细的分析单元:结构体或联合体的类型布局、声明的类型、常量的值、函数的函数体。

四类分析单元,组成一张依赖图 类型布局 struct / union 的大小与对齐 声明类型 函数或变量声明的类型 常量值 const 声明在 comptime 下的实际值 函数体 只能依赖别人,不会被别人依赖 每个单元还依赖源码里对应片段的哈希值 哈希不变则不重新分析,分析后值不变则停止向下传播

分析一个单元时,编译器记录它依赖了哪些其他单元,也记录它依赖了源码哪一段的哈希。改一行代码,只有对应哈希变化,只有真正依赖这段哈希的单元被标记过期、重新分析。

文章里举的例子很直白:把一个常量从42改成43,依赖图先标记这个常量的值,重新算出来发现值确实变了,于是继续标记读取它的两个函数体。这两个函数体没有别的单元依赖它们,传播到此为止。如果改动只是加了个空格、值根本没变,传播在第一步就停下了。

代码生成和链接这两块,作者自己的材料还没写完,目前公开的成熟部分只到前端和语义分析这一层。

锐评:语言设计替编译器提前还的债

这轮加速拿出来讲,不是因为团队发明了新的缓存算法,是 Zig 从设计早期就在为这件事让路。作者说得直白:大多数现代语言理论上都能支持类似的增量编译,但某些语言特性会让这件事难得多——他没有点名哪门语言做不到,只强调设计决定难度。这句话比听起来更狠,因为它把责任推回了每一种语言当年的选择。

这有点像早年铁路轨距之争。轨距一旦铺下去,后面想统一,代价是整条线拆了重建。Zig 现在拿出的毫秒级重编译,不是运营优化,是当年在"能不能把依赖切细、切准"这个问题上,提前认下的工程债,现在到了兑现的时候。团队为此在issue里争了好几年,部分改动颇具争议。

这笔债不是白得的。作者也承认,代码生成和链接部分的实现,现在还没写完;要把这套体系搬到已经存在多年、语言特性早就定型的项目上,几乎不可能,只能靠新语言在设计阶段就选对方向。

对编译器和语言工具链开发者来说,这篇技术博客比演示视频更值得读——它给出的是"可切分依赖"这个设计思路,而不是一份可以直接抄的实现。对已经在用Zig的工程师,现实的判断是:想尝鲜就上master,想稳定就等0.17.0,别指望现在的稳定版能给你这个体验。

接下来最该盯的不是这次演示的毫秒数,是0.17.0什么时候发布、代码生成和链接部分补全后速度会不会打折扣,以及这套依赖切分方式放到更大规模的代码库里还能不能扛住。