487 天!Roc 编译器从 Rust 重写为 Zig,功能对等且编译速度快百倍!
实现功能对等
过去一年半,Roc 编译器团队将 30 万行 Rust 代码重写为 Zig 代码,新编译器功能与原编译器对等。达成这一里程碑后,得以更新 2024 年开发的 WASM - 4 游戏 Rocci Bird,其更新后的源代码更简洁,使用 `roc build --opt=size` 命令输出的 WASM 二进制文件仅 31KB,是原编译器生成文件的一半。不过这并非正式版本发布,计划在今年晚些时候推出 0.1.0 版本。同时,多位贡献者为新编译器做出了重要贡献。重写耗时 487 天,比 Bun 用 11 天将约 50 万行 Zig 代码重写为 Rust 代码长了 476 天,原因在于 Bun 是直接移植,而 Roc 要进行大量改动。
热代码加载与跨平台编译二进制文件
Roc 新编译器开发时会自动热代码加载,如运行 `roc server.roc` 启动 Web 服务器,修改代码后下次请求会自动使用新代码。还支持跨平台编译,运行 `roc build --target=x64musl` 可构建能在 Alpine Linux 上运行的静态二进制文件,且相同输入源代码在不同系统生成相同输出字节。
支持字符串插值的模式匹配
新编译器引入模式匹配中的字符串插值特性,如处理 HTTP 请求的代码中,`"/users/${id}"` 这种语法在编译时具有类型安全性,且不会产生堆内存分配。可在新的 roc - lang.org 主页体验该语法。
为何要彻底重写?
Roc 采用自动内存管理,是首个在非学术领域实现通过 lambda 集合特化进行多态去函数化的语言,闭包捕获不进行堆内存分配。但原实现遇到困难,新编译器在 Ayaz Hafiz 原型开发基础上成功实现该功能。此外,多位贡献者提出重写编译器不同部分,因此决定彻底重写。
为何选择 Zig?
团队基于使用 Rust 和 Zig 的经验,决定用 Zig 构建整个编译器。主要考虑因素有:编译时间上,Rust 的 `cargo` 编译时间长,预计 Zig 重写后会大幅缩短;内存控制方面,Rust 生态系统假设使用单一全局分配器,而 Zig 支持细粒度分配器和数组结构体布局;生态系统相关性上,两个生态系统中与需求相关的包都少,但 Zig 中可用的小众功能代码更多;内存不安全代码辅助支持上,Rust 隔离内存不安全代码,而 Zig 有更多特性确保其正确性,Roc 代码中使用 `unsafe` 较频繁,所以 Zig 更有吸引力。
没有借用检查器的日子
内存安全问题中,约 70% 是内存安全问题,Rust 和 Zig 在处理部分问题方式相同,16.4% 的使用已释放内存错误可被 Zig 的 `ReleaseSafe` 运行时检查、Rust 的借用检查器等捕获。`ReleaseSafe` 有运行时开销且可能触发 panic,`ReleaseFast` 在生产环境跳过检查。Rust 代码依赖项中的 `unsafe` 代码段存在内存不安全风险,一些 Rust 项目曾出现内存不安全的 CVE 漏洞。Roc 考虑自身使用 `unsafe` 频率高,且一些 Zig 项目有出色表现,决定使用 Zig。
重写后的内存安全情况
从 Roc 问题跟踪器的 bug 报告分类统计看,基于 Rust 的编译器有 21 个内存损坏的 bug,基于 Zig 的有 10 个。Rust 编译器的内存损坏 bug 多因编译生成的机器指令导致,Zig 编译器的 10 个中有 8 个是错误编译问题,2 个是使用已释放内存的 bug。不同工具选择对项目影响不显著,Roc 编译器与 Bun 面临不同挑战。
编译时间
Zig 重写后,`zig build --watch -fincremental` 可在约 35 毫秒内重新编译约 45 万行 Zig 代码,但 Zig 0.16.0 版本有 bug 使 `-fincremental` 失效,需等待下一个稳定版本。对比不同版本的编译器编译时间,Rust 编译速度有显著提升,但 Zig 的 35 毫秒仍遥遥领先,且代码行数更多。
内存控制:零解析反序列化
Roc 新的磁盘缓存系统采用零解析反序列化技术,编译器数据结构表示为带 32 位指针索引的数组,可直接写入和反序列化,反序列化速度等同于磁盘 I/O 速度。连续运行 `roc check` 或 `roc test` 可快速加载缓存结果。但这种方法有安全风险,Rust 的借用检查器无法处理索引对应数组的问题,`compact_arena` 库在特定场景也无法使用。
