Zig 的增量编译内部
作为 Zig 核心团队的成员,我参与的最具影响力的项目之一是将增量编译实现到 Zig 编译器中。这个功能允许编译器检测自上次构建以来,哪些单独的函数和声明已发生更改,只重新编译该代码,并将生成的字节直接修补到输出二进制文件中,从而使重建速度极快。Zig 项目为实现这一功能已经努力了很长时间,并且在过去的几个版本周期里,它终于从一个概念验证质量的特性转变为一个适用于现实项目的特性,Zig 核心团队的大多数成员每日都在使用这个功能。今天,使用 Zig 的增量编译,您可以在几毫秒内对真实的复杂应用程序进行更改。但请不要只听我这么说!这是一个简单的视频(没有音频),展示了我如何使用 Zig 快速对一个像素编辑应用程序 Fizzy 进行更改和测试。初始构建大约需要 5 秒,然后每次我进行更改时,重建完成的时间为 50-70 毫秒。为了这个演示,我不得不将 Fizzy 升级到 Zig 的主分支。这是因为虽然 Zig 0.16.0 确实支持增量编译,但它缺少一些重要的链接器功能,而这些功能已经实现。这意味着如果您希望坚持使用打标签的 Zig 版本,您可能需要等到 0.17.0 发布时才能尝试这个;抱歉! 对 Fizzy 的一些随机更改进行快速增量重建 如果您已经信服并只想知道如何使用这个功能,那太好了!请前往此帖的最后部分了解更多信息。但也许您有理由对这是否适用于大多数项目抱有怀疑,或者像我一样,您只是喜欢学习这种东西是如何运作的。对于所有这些朋友,让我们深入探讨细节! 处理源文件 Zig 编译器的管道可以分为几个不同的部分,我们将按顺序查看。第一部分在整个源文件的粒度上工作,基本上包括在循环中运行以下过程:从磁盘读取源文件 将该文件解析为 AST 使用名为 “AstGen” 的过程将该 AST 转换为一种称为 “ZIR” 的格式 如果您感兴趣,ZIR(Zig 中间表示)是一种无类型的 SSA 形式 IR——但如果您不知道这意味着什么,也没关系,因为在这里并不重要。我们关心的是将整个源文件转换为另一种格式。当 AstGen 运行时,它会学习源文件中的所有 Zig 导入(@import("foo.zig")),因此我们可以在所有导入的文件上重复这个完整的过程。因此,通过在循环中运行这个过程,我们最终会发现编译中的每个 Zig 源文件,并将它们全部转换为 ZIR。 Zig 编译器中的文件处理管道 管道的这一部分实际上有几个有用的特性:对每个文件的处理是该文件内容的纯函数,不涉及任何共享或外部状态;解析和 AstGen 本身都相当快速:在我的笔记本电脑上,同时对 Zig 编译器的整个 src/ 目录运行它们(完全没有并行性)大约需要 920 毫秒;得益于 Zig 对数据导向设计模式的使用,ZIR 可以通过一次 writev/readv 系统调用轻松地写入和读取磁盘,没有 “序列化” 步骤。这些特性带来了两个不错的结果。首先,假设每个源文件一个 “任务”,这个整个过程是极其并行的。这意味着每当我们从导入中发现一个新源文件时,我们可以在线程池上轻松运行它,将一个任务排入队列——唯一的共享状态(我们会用互斥锁保护)是一个哈希集,用于跟踪我们已经看到的文件路径。其次,或许更重要的是,这些特性使得实现增量编译对于管道的这一部分变得非常简单。我们所需做的就是将每个源文件生成的 ZIR 缓存在磁盘上,并仅在我们检测到文件发生更改时才重新构建。这两项优化在 Zig 中默认启用多年——经过实战考验,使这一部分的管道在大多数情况下几乎是瞬时的。如果您正在使用 Zig,可以通过 stderr 上的进度输出看到它有多快——当它说 “AST 降低” 时,管道的这一部分正在运行。我猜许多 Zig 用户只在首次运行编译器时才会注意到这一点(因为在第一次运行时,编译器需要为整个 Zig 标准库和 compiler_rt 完成这项工作)。好吧,我们让这一部分变得快速了!那很好,但坏消息是这只是简单的部分——许多编译器已经可以做到这种缓存。从这里开始,事情将变得更棘手。 语义分析 管道的下一部分可以说是最重要的:语义分析。这包括类型检查和编译时求值。语义分析的职责是
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡