文章摘要
Zig核心团队成员介绍了增量编译的实现原理,该功能能检测代码变化并仅重新编译修改部分,将重建时间从数秒缩短至50-70毫秒,已从概念验证发展为适用于实际项目的成熟特性。
文章总结
作为Zig核心团队成员,我参与的最具影响力的项目之一是在Zig编译器中实现增量编译。该功能允许编译器检测自上次构建以来哪些函数和声明发生了变化,仅重新编译这些代码,并将生成的字节直接修补到输出二进制文件中,从而使重新构建速度极快。
Zig项目长期致力于此功能,经过几个发布周期,它已从概念验证阶段发展为适用于实际项目且大多数Zig核心团队成员日常使用的功能。如今,使用Zig的增量编译,你可以在毫秒级时间内对真实复杂应用进行修改。
处理源文件
Zig编译器的流水线可分为几个部分。第一部分以整个源文件为粒度,循环执行以下过程:从磁盘读取源文件,将其解析为AST,再通过AstGen将AST转换为ZIR格式。此过程具有几个有用特性:每个文件的处理是该文件内容的纯函数,不涉及共享或外部状态;Parse和AstGen本身速度很快;ZIR可以轻松写入和读取磁盘,无需序列化步骤。
这些特性带来两个好处:首先,每个源文件的任务可以轻松并行化;其次,实现增量编译非常简单,只需缓存每个源文件生成的ZIR,并在检测到文件更改时重新构建即可。这两个优化已在Zig中默认启用多年,经过实战检验,使这部分流水线在大多数情况下几乎瞬间完成。
语义分析
流水线的下一部分是语义分析,包括类型检查和编译时求值。语义分析是编译器中最难处理增量的部分,语言设计在此处至关重要。关键在于将编译拆分为多个可以独立分析的片段,并建立依赖图。
在Zig编译器中,这些片段称为"分析单元",共有四种类型:结构体或联合体类型的布局、容器级声明的类型、容器级const声明的值、运行时函数的主体。在分析特定单元时,我们会填充该单元依赖的其他单元集合。依赖关系包括对其他分析单元的依赖,以及对源代码片段的依赖。当源代码发生变化时,依赖的分析单元将被标记为"过时"并重新分析。
代码生成
代码生成是将语义分析生成的AIR转换为类似机器指令的阶段。与整个文件处理类似,代码生成也是可并行化的任务,不同函数之间没有共享状态。在增量编译方面,此阶段最为简单,因为AIR和MIR都以单个函数为粒度,与增量编译的工作粒度相同,因此无需缓存AIR或MIR。
链接
增量链接是一个难题,当控制整个编译流水线时,可以采用更简单的设计:将链接器与编译器紧密集成。链接器接收MIR后,将其转换为实际机器码,生成重定位信息,并在输出节中预留空间。对于增量链接,我们需要在知道二进制文件完整内容之前写入机器码、分配地址和应用重定位,并且能够稍后更新这些内容。
为解决此问题,Zig编译器引入了link.MappedFile抽象,它将输出文件映射到内存,并跟踪文件中的节点树。API用户可以添加节点或扩展节点,如果父节点中没有空间,MappedFile会移动其他节点以腾出空间。通过使用指数增长因子,我们摊销了移动成本,使其在实际中极为罕见。
刷新
在刷新阶段,我们需要遍历引用图以确定哪些函数/声明被实际引用,然后告诉链接器所有从Zig代码导出的全局符号,最后调用链接器的刷新函数完成剩余链接工作。我们希望在每次更新时尽可能少做工作,保持O(1)复杂度。
追踪更新
通过Tracy分析器,我们可以直观看到增量更新的过程。在一次对Fizzy的37毫秒更新中,前6毫秒包括线程池处理所有文件、遍历文件导入图、关联新旧ZIR等工作。随后是语义分析、代码生成和链接,这部分仅需约1.6毫秒。剩余31毫秒主要花费在resolveReferencesInner函数上,用于确定哪些Zig声明被引用。这显示了仍有大量效率提升空间。
如何使用
目前,增量编译主要适用于x86_64-linux目标。使用方式很简单:运行zig build --watch -fincremental命令。--watch参数让构建系统监视文件变化并触发重新构建,-fincremental参数指示使用增量编译。你也可以修改build.zig为特定编译启用增量编译。初始构建后,编辑源文件并保存,即可立即看到重新构建结果。
请注意,增量编译尚未稳定,可能存在错误。如果遇到问题,欢迎在Zig仓库中提交issue。感谢所有帮助测试此功能的用户,以及向Zig软件基金会捐赠的人士。
评论总结
根据评论内容,总结主要观点如下:
1. 对Zig编译器增量编译工作的赞赏
- 评论3(applfanboysbgon)指出行业长期忽视编译速度问题,Zig在此领域做出了高价值贡献。
- 关键引用:"It disappoints me how unseriously the industry has taken compilation speed for so long."
- "I'm glad to see Zig doing incredibly valuable, high-impact work here."
- 评论4(steveklabnik)称赞Zig工具链持续出色,并指出其语言设计为增量编译做了针对性调整。
- 关键引用:"Zig's toolchain work is continually impressive."
- "Zig has had its design tweaked over the years...specifically so that it is easier to support fast incremental compilation."
2. 对增量编译设计的具体疑问
- 评论5(thefaux)质疑调试构建中生成巨型二进制文件的设计,建议采用多个小型共享库替代。
- 关键引用:"why are they insisting on building a giant binary for debug builds that contains all of the code?"
- "a simpler approach is to generate many smaller shared libraries...and link them in to the final binary."
3. 对编译速度与语言设计权衡的反思
- 评论4(steveklabnik)以Rust为例,指出早期发布1.0版本可能牺牲了编译速度优化机会。
- 关键引用:"This is something I wish that we had done with Rust."
- "if had a few more years to bake things, maybe we could have made compile times way faster."
4. 其他技术性询问
- 评论1(remywang)询问该功能是否仅适用于调试构建。
- 关键引用:"Does this work for release builds or just debug builds now?"
- 评论2(hoppp)询问Zig编译器是否支持C语言编译。
- 关键引用:"The zig compiler can compile C so will it work with C also?"