链接:https://ziglang.org/news/goodbye-cpp/
声明:本文为 CSDN 翻译,未经许可禁止转载。
在这次变更之前,Zig 代码库由两个编译器组成:

旧编译器:统共包含 8 万行 C++ 代码,加上新编译器共享 Zig 代码。
新编译器:统共包含 25 万行 Zig 代码。
新编译器的速率更快,利用的内存更少,并得到了积极的掩护和增强。同时,没有人想碰旧编译器,但是通过源代码构建新编译器时须要用到旧编译器。
这意味着,新的 Zig 措辞特性必须实现两次:在新代码库中实现一次,然后在旧代码库中再实现一次——这是一个巨大的痛点,尤其这两个编译器的设计早已大相径庭。
此外,用 C++ 实现的 Zig 最初利用的策略与 D 编译器相同:在进程退出之前不开释任何内存。但随着编译时实行代码成为该措辞的标志性功能之一,加之利用同一个编译单元来处理统统,项目的规模越来越大,这个设计决策就有点不合时宜了。
办理方案这个问题很有趣,有很多办理方案,但每一种都有一定的弊端。在搞清楚问题之后,我们就各种可能性展开了快速的头脑风暴会议,每个发起都有自己的优缺陷。
放弃自我编译Odin 就采取了这种方法。
这个方法可以办理全体问题,但缺陷是我们必须放弃 Zig,转而利用 C。我不同意,由于 Zig 带来的改进太诱人了,我不想放弃:例如,我们利用的一些面向数据的设计技能无法通过 C/C++ 实现。
利用旧版本的编译器这是 Rust 和许多其他措辞采取的方法。
这种方法的一大缺陷是,根据源代码构建任何提交都须要经由繁芜的操作。例如,假设你考试测验实行 git bisect,有时 git 会检出一个旧版本的提交,但脚本无法利用这些源代码构建,由于构建编译器的二进制文件不是精确版本。当然,这个问题并不是无法办理,但势必会给开拓职员带来许多不必要的麻烦。
此外,构建编译器也会受到目标平台上已有二进制文件的限定。例如,假设没有 riscv64 版的编译器,就无法在 riscv64 硬件上利用源代码构建。
因此关键问题在于,这种方法无法充分支持能够在任何系统上构建任何提交的需求。
将编译器编译成 C 代码这是 Nim 采取的方法。
与前面的策略比较,这种方法的好处是可以将天生的 C 代码提交至源代码掌握,比较方便,但如果这些 C 代码只能用于特定平台,实际效果也是一样的。
我不太清楚若何才能天生不依赖于平台的 C 代码(像 Nim 那样),但我看到他们的描述中说:“支持的 CPU/OS 组合比旧的 csources 代码库更多”。这意味着,只管他们的代码可以在许多 CPU/OS 的组合上编译,但这并不一定表示这些 C 代码具有可移植性。此外,这些代码与主编译器分别保存在不同的代码库中,以是并不能办理前一种策略碰着的问题。
我探索了这种可能性,创造天生的 C 代码不仅只能用于特定平台,而且代码量非常大。我们的编译器会天生一个 80 MiB 的 C 文件。虽然我们可以通过 C 后端增强来改进,但与其接管如此大规模的扩展,还不如直接将二进制文件提交到 Git 代码库。
将编译器编译成 C 代码,之后直接掩护 C 代码这种方法我从多年前就想试试,最近终于开始研究了一下。很明显的一大缺陷是,清理自动天生的 C 代码非常困难,而且依然须要掩护两种编译器实现,随意马虎打击贡献者的积极性——谁会乐意用 Zig 和 C 重复两次相同的事情?
将编译器编译成大略的虚拟机有一次,在谈论编译器自举时,Drew DeVault 提到了 OCaml 的策略。于是我有了一个想法:可以建立一个自举专用的后端。
不过我认为 Zig 和 OCaml 之间还有一些差异。Zig 只有一个虚拟机平台,它是跨平台的,可以通过 LLVM 进行优化——这便是 WebAssembly,利用 WASI 作为操作系统抽象层。
探索思路
紧张思路是,利用一个非常小的 wasm 二进制作为 stage1 内核,提交到源代码掌握,这样就可以用它来编译源代码中的任何提交。我们供应了一个 C 编写的 WASI 阐明器,然后用它将 Zig 编译器代码编译成 C 代码。之后用系统的 C 编译器对 C 代码进行编译和连接,天生 stage2 二进制文件。接着,stage2 二进制文件就可以通过反复的 zip build 从源代码进行构建。
wasm 二进制文件是通过 zig build update-zig1 天生的,后者利用了 LLVM 后端来天生一个针对 wasm32-wasi 平台、generic+bulk_memory CPU 的 ReleaseSmall 二进制文件。该二进制文件中 C 之外的所有后端都是禁用的,这样产生的文件仅有 2.6MiB。然后再通过 wasm-opt -Oz --enable-bulk-memory 进行优化,压缩到 2.4MiB。末了,用 zstd 进一步压缩至 637KB。个中还包括了 zstd 解码器(用 C 实现),但这是值得的,由于 zstd 的实现基本不会改变,而且它每次都能给 wasm 二进制文件节省下 1.8MiB。
因此,我们用 4 千行可移植的 C 代码更换了原来 8 万行的 C++ 代码。这些代码仅利用了标准 libc 函数,且不依赖于任何 POSIX 的头文件,也不依赖于 windows.h。操作系统互操作层完备抽象到了几个 WASI 函数中,由 WASI 阐明器卖力实现:
(import \"大众wasi_snapshot_preview1\"大众 \公众args_sizes_get\"大众 (func (;0;) (type 3)))
(import \"大众wasi_snapshot_preview1\"大众 \公众args_get\"大众 (func (;1;) (type 3)))
(import \"大众wasi_snapshot_preview1\"大众 \"大众fd_prestat_get\"大众 (func (;2;) (type 3)))
(import \公众wasi_snapshot_preview1\"大众 \"大众fd_prestat_dir_name\"大众 (func (;3;) (type 6)))
(import \公众wasi_snapshot_preview1\"大众 \"大众proc_exit\"大众 (func (;4;) (type 11)))
(import \公众wasi_snapshot_preview1\"大众 \"大众fd_close\"大众 (func (;5;) (type 8)))
(import \"大众wasi_snapshot_preview1\"大众 \公众path_create_directory\"大众 (func (;6;) (type 6)))
(import \"大众wasi_snapshot_preview1\"大众 \"大众fd_read\"大众 (func (;7;) (type 5)))
(import \"大众wasi_snapshot_preview1\"大众 \公众fd_filestat_get\"大众 (func (;8;) (type 3)))
(import \"大众wasi_snapshot_preview1\"大众 \公众path_rename\公众 (func (;9;) (type 9)))
(import \公众wasi_snapshot_preview1\"大众 \"大众fd_filestat_set_size\公众 (func (;10;) (type 36)))
(import \公众wasi_snapshot_preview1\"大众 \公众fd_pwrite\公众 (func (;11;) (type 28)))
(import \"大众wasi_snapshot_preview1\公众 \"大众random_get\"大众 (func (;12;) (type 3)))
(import \公众wasi_snapshot_preview1\公众 \"大众fd_filestat_set_times\公众 (func (;13;) (type 51)))
(import \公众wasi_snapshot_preview1\公众 \"大众path_filestat_get\"大众 (func (;14;) (type 12)))
(import \"大众wasi_snapshot_preview1\"大众 \公众fd_fdstat_get\公众 (func (;15;) (type 3)))
(import \"大众wasi_snapshot_preview1\公众 \"大众fd_readdir\"大众 (func (;16;) (type 28)))
(import \"大众wasi_snapshot_preview1\"大众 \"大众fd_write\公众 (func (;17;) (type 5)))
(import \"大众wasi_snapshot_preview1\公众 \公众path_open\"大众 (func (;18;) (type 52)))
(import \"大众wasi_snapshot_preview1\公众 \公众clock_time_get\公众 (func (;19;) (type 53)))
(import \"大众wasi_snapshot_preview1\公众 \公众path_remove_directory\"大众 (func (;20;) (type 6)))
(import \公众wasi_snapshot_preview1\"大众 \"大众path_unlink_file\"大众 (func (;21;) (type 6)))
(import \"大众wasi_snapshot_preview1\"大众 \公众fd_pread\公众 (func (;22;) (type 28)))
为了让 Zig 编译器把自己编译成 C,只须要利用这些系统调用。
Jacob Young和我一起,在andrewrk/zig-wasi的根本上完成了这个 WebAssembly/WASI 阐明器。我用 Zig 建立了初版,借助 Zig 丰富的标准库和安全机制探索了这个思路。这个阐明器不会提前解码 wasm 模块,而是直策应用文件偏移量作为程序计数器。虽然它能正常事情,但太慢了,对编译器进行阐明实行须要好几个小时,而用原活气器码只需大约 5 秒钟。
因此,Jacob 改进了该项目,引入了另一个指令集和更多的优化,还有一些其他技巧,将性能提升到了可接管的程度。同时,我将 Zig 代码转成了纯 C。
我们一起努力了大约两个星期,相互将对方的代码合并到自己的分支中,互换心得、分享成功的喜悦。我非常感谢 Jacob 在这个项目上的努力,特殊是他一丝不苟地改进 Zig 的 C 后端,才让这个项目得以成功。
在观点得到证明后,Jacob 意识到,将 WebAssembly 转成 C,要比直接阐明实行更快。这实际上便是 JIT 编译,但更大的好是,我们的自举工具实际上是系统的 C 编译器。
WebAssembly Binary Toolkit 项目里有一个 wasm2c 工具,但我们并没有移植或分叉——Jacob 从零开始创建了一个 wasm2c 的实现。这个实现没有考虑通用性,只包含了在编译器编译自己时须要调用的系统调用。
以是,这个版本的 wasm2c 只有 4 千行代码,也不依赖 C++,采取了更简洁的办法,没有实现任何沙盒、安全特性等。
新的构建过程
下面是新的构建过程:
Building CXX object CMakeFiles/zigcpp.dir/src/zig_llvm.cpp.o
Building CXX object CMakeFiles/zigcpp.dir/src/zig_llvm-ar.cpp.o
Building CXX object CMakeFiles/zigcpp.dir/src/zig_clang.cpp.o
Building CXX object CMakeFiles/zigcpp.dir/src/zig_clang_driver.cpp.o
Building CXX object CMakeFiles/zigcpp.dir/src/zig_clang_cc1_main.cpp.o
Building CXX object CMakeFiles/zigcpp.dir/src/zig_clang_cc1as_main.cpp.o
Building CXX object CMakeFiles/zigcpp.dir/src/windows_sdk.cpp.o
Linking CXX static library zigcpp/libzigcpp.a
Built target zigcpp
Building C object CMakeFiles/zig-wasm2c.dir/stage1/wasm2c.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/decompress/huf_decompress.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/decompress/zstd_ddict.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/decompress/zstd_decompress.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/decompress/zstd_decompress_block.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/common/entropy_common.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/common/error_private.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/common/fse_decompress.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/common/pool.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/common/xxhash.c.o
Building C object CMakeFiles/zig-wasm2c.dir/stage1/zstd/lib/common/zstd_common.c.o
Linking C executable zig-wasm2c
Built target zig-wasm2c
Converting ../stage1/zig1.wasm.zst to zig1.c
Building C object CMakeFiles/zig1.dir/zig1.c.o
Building C object CMakeFiles/zig1.dir/stage1/wasi.c.o
Linking C executable zig1
Built target zig1
Running zig1.wasm to produce zig2.c
Running zig1.wasm to produce compiler_rt.c
Building C object CMakeFiles/zig2.dir/zig2.c.o
Building C object CMakeFiles/zig2.dir/compiler_rt.c.o
Linking CXX executable zig2
Built target zig2
Building stage3
总结:
利用系统 C 编译器编译 zig-wasm2.c;
利用 zig-wasm2.c 将 zig1.wasm.zst 转换成 zig1.c;
利用系统 C 编译器编译 zig1.c;
a.把稳 zig1 只启用了 C 后端。
利用 zig1 将 Zig 编译器编译成 zig2.c;
利用系统 C 编译器编译 zig2.c;
a.这个编译器的逻辑是精确的,但它的机器码是由系统 C 编译器优化的,而不是它自己优化的。以是我们连续进行第六步,得到一个自我编译后的性能特性。
zig2 build(利用旧版本 Zig 编译 Zig 的标准构建流程);
如果用末了一步产生的结果再次编译 Zig,会得到同样的字节码。也便是说,zig3、zig4 是完备相同的。以是全体过程结束,末了得到的二进制文件可以去掉后缀,直接命名为 zig。
wasm 二进制的更新仅限于有重大更新,或新功能影响到编译器构建自身的时候。例如,编译器中的 bug 修复不会影响编译器编译自身,因此可以忽略。但如果 Zig 编译自身时必须修复该 bug,那么就须要更新 wasm 二进制。与之相似,当措辞改变、编译器须要利用新功能来编译自身时,就要更新 wasm 二进制文件。
更新stage1/zig1.wasm.zst的方法如下:
zig build update-zig1
性能
我网络了两个性能数据:
数据#1:利用make -j8 install 从源代码编译,配置为-DCMAKE_BUILD_TYPE=Debug:
old: 8m12s with 11.3 GiB peak RSS
new: 9m59s with 3.8 GiB peak RSS
数据#2:利用ninja install从源代码编译,配置为-DCMAKE_BUILD_TYPE=Release:
old: 13m20s with 10.3 GiB peak RSS
new: 10m53s with 3.2 GiB peak RSS
个中最关键的是构建时内存的需求量。一个只有 4~8GiB 的 RAM 能否编译 Zig 是非常主要的,这决定了是否可以用 GitHub 认证的 Action。
未来的操持
有一点我要承认,只管这次改动大获全胜,但还是有一个地方退步了,即能否在固定次数内实现 Zig 的自举。
在这之前,从源代码开始的构建过程没有涉及任何二进制块,除了系统的 C/C++ 编译器之外。但这次改动之后,它利用了 WebAssembly 的二进制,这并不是源代码,而是一个构建结果,有些人可能非常看重这一点。
这是须要付出的代价,但我认为这些代价是值得的。考虑到官方措辞规格以及 Zig 越来越受欢迎的现状,我们会看到更多的第三方项目开始用 C 来实现 Zig。
我愿把这次改动之前的版本标记为 1.0,且这次改动也并不在 Zig 软件基金会的操持内。当然,事情可能会改变,这只是目前的操持。
此外,这次改动还去掉了 -fstage1 标志,这个标志可以让 Zig 用户选择旧版本编译器来代替新版本——这是利用异步函数的唯一方法,而异步函数功能在新的编译器中还没有实现。
我建议须要利用 -fstage1 标志的用户连续利用 0.10.0,当 0.10.1 发布后进行升级,终极升级到 0.11.0,该版本将会支持异步函数。把稳 Zig 采取语义版本号,因此本文中所说的统统都不会进入 0.10.1 发布,。0.10.1 只会包含 master 分支上的 bug 修复。
措辞的发展
从更积极的方面来看,这次改动意味着所有操持中的措辞改动都可以更快地进行。当我们分开了旧代码的束缚,Zig 0.11.0 的发布迭代会更快。
有了这个改动,我们就可以像试用标准库那样,立即试用措辞上的新改动。
在这个改动合并到主分支时,标签“stage1”下已经有 650 个问题,而这些问题所针对的代码都可以删掉了!
以是,理论上我们可以立即关闭这些问题,不过我哀求关闭这些问题时必须编写相应的测试用例,或者证明相应的部分已经有测试覆盖了。大概这须要付出更多的努力,但这正是我们当前的事情。