编程语言分为两类:垃圾回收型编程语言和需要手动内存管理的编程语言。前者的示例包括 Kotlin、PHP 或 Java 等众多语言。后者的示例包括 C、C++ 或 Rust。一般而言,高级编程语言更有可能将垃圾回收作为一项标准功能。本文将重点介绍此类垃圾回收型编程语言以及如何将其编译为 WebAssembly (Wasm)。首先,什么是垃圾回收(通常称为 GC)?
浏览器支持
垃圾回收
简而言之,垃圾回收的概念是指尝试回收由程序分配但不再引用的内存。这种内存称为垃圾。实现垃圾回收的方法有很多。其中之一是引用计数,其目标是统计内存中对象的引用数量。当没有对某个对象的更多引用时,该对象可以被标记为不再使用,从而准备好进行垃圾回收。PHP 的垃圾回收器使用引用计数,您可以使用 Xdebug 扩展程序的 xdebug_debug_zval()
函数来了解其内部运作方式。请考虑以下 PHP 程序。
<?php
$a= (string) rand();
$c = $b = $a;
$b = 42;
unset($c);
$a = null;
?>
该程序会将转换为字符串的随机数分配给一个名为 a
的新变量。然后,它会创建两个新变量(b
和 c
),并为其赋值 a
。之后,它会将 b
重新分配给数字 42
,然后取消设置 c
。最后,它将 a
的值设置为 null
。使用 xdebug_debug_zval()
为程序的每个步骤添加注解后,您可以看到垃圾回收器的引用计数器在工作。
<?php
$a= (string) rand();
$c = $b = $a;
xdebug_debug_zval('a');
$b = 42;
xdebug_debug_zval('a');
unset($c);
xdebug_debug_zval('a');
$a = null;
xdebug_debug_zval('a');
?>
上述示例将输出以下日志,您可以从中了解在执行每个步骤后对变量 a
值的引用次数如何减少,这在代码序列中是合理的。(当然,您的随机数字会有所不同。)
a:
(refcount=3, is_ref=0)string '419796578' (length=9)
a:
(refcount=2, is_ref=0)string '419796578' (length=9)
a:
(refcount=1, is_ref=0)string '419796578' (length=9)
a:
(refcount=0, is_ref=0)null
垃圾回收还有其他挑战,例如检测循环,但对于本文而言,只要对引用计数有基本的了解即可。
编程语言以其他编程语言实现
这可能听起来很奇怪,但编程语言是用其他编程语言实现的。例如,PHP 运行时主要采用 C 语言实现。您可以在 GitHub 上查看 PHP 源代码。PHP 的垃圾回收代码主要位于 zend_gc.c
文件中。大多数开发者都会通过操作系统的软件包管理器安装 PHP。不过,开发者也可以从源代码构建 PHP。例如,在 Linux 环境中,步骤 ./buildconf && ./configure && make
会为 Linux 运行时构建 PHP。但这也意味着,PHP 运行时可以针对其他运行时进行编译,例如 Wasm。
将语言移植到 Wasm 运行时的传统方法
无论 PHP 运行在哪个平台上,PHP 脚本都会编译为相同的字节码,并由 Zend Engine 运行。Zend Engine 是 PHP 脚本语言的编译器和运行时环境。它由 Zend 虚拟机 (VM) 组成,该虚拟机由 Zend 编译器和 Zend 执行器组成。以 C 等其他高级语言实现的 PHP 等语言通常具有针对特定架构(例如 Intel 或 ARM)的优化,并且每个架构都需要不同的后端。在这种情况下,Wasm 代表了一种新架构。如果虚拟机具有特定于架构的代码(例如即时编译 [JIT] 或预编译 [AOT]),则开发者还需要为新架构实现 JIT/AOT 后端。这种方法非常有用,因为通常只需针对每个新架构重新编译代码库的主要部分即可。
鉴于 Wasm 的底层特性,自然而然地,我们也可以尝试在 Wasm 中采用相同的方法:将主虚拟机代码及其解析器、库支持、垃圾回收和优化器重新编译为 Wasm,并根据需要为 Wasm 实现 JIT 或 AOT 后端。从 Wasm MVP 开始,这种做法就已可行,并且在许多情况下在实践中也能取得良好效果。事实上,WordPress Playground 的强大动力来自编译为 Wasm 的 PHP。如需详细了解该项目,请参阅使用 WordPress Playground 和 WebAssembly 构建浏览器内 WordPress 体验一文。
不过,PHP Wasm 在浏览器中运行时,是在主机语言 JavaScript 的上下文中运行。在 Chrome 中,JavaScript 和 Wasm 在 V8 中运行,V8 是 Google 的开源 JavaScript 引擎,可按 ECMA-262 中指定的方式实现 ECMAScript。V8 已经有垃圾回收器。这意味着,如果开发者使用的是编译为 Wasm 的 PHP,最终会将移植语言 (PHP) 的垃圾回收器实现发送到已具有垃圾回收器的浏览器,这听起来很浪费。这正是 WasmGC 的用武之地。
让 Wasm 模块在 Wasm 的线性内存上构建自己的 GC 的旧方法的另一个问题是,Wasm 自己的垃圾回收器与编译为 Wasm 语言的内置垃圾回收器之间没有任何交互,这往往会导致内存泄露和收集尝试效率低下等问题。让 Wasm 模块重复使用现有的内置 GC 可避免这些问题。
使用 WasmGC 将编程语言移植到新的运行时
WasmGC 是 WebAssembly 社区群组的提案。当前的 Wasm MVP 实现只能处理线性内存中的数字(即整数和浮点数),随着引用类型提案的发布,Wasm 还可以保留外部引用。WasmGC 现在添加了结构体和数组堆类型,这意味着支持非线性内存分配。每个 WasmGC 对象都有固定的类型和结构,这使得虚拟机可以轻松生成高效的代码来访问其字段,而不会像 JavaScript 等动态语言那样存在deoptimizations的风险。因此,该提案通过结构体和数组堆类型为 WebAssembly 添加了对高级管理语言的高效支持,这些类型可让以 Wasm 为目标平台的语言编译器与主机虚拟机中的垃圾回收器集成。简而言之,这意味着,使用 WasmGC 将编程语言移植到 Wasm 意味着编程语言的垃圾回收器不再需要包含在移植中,而是可以使用现有的垃圾回收器。
为了验证这项改进在实际中的效果,Chrome 的 Wasm 团队使用 C、Rust 和 Java 编译了 Fannkuch 基准测试(该测试会在运行时分配数据结构)。C 和 Rust 二进制文件的大小可能介于 6.1 K 到 9.6 K 之间,具体取决于各种编译器标志,而 Java 版本要小得多,只有 2.3 K!C 和 Rust 不包含垃圾回收器,但它们仍然会捆绑 malloc/free
来管理内存,Java 在这里体积较小的原因在于它根本不需要捆绑任何内存管理代码。这只是一个具体示例,但它表明 WasmGC 二进制文件有可能非常小,而且这还没有进行任何针对大小的优化工作。
查看 WasmGC 移植的编程语言的实际运作
Kotlin Wasm
得益于 WasmGC,第一个移植到 Wasm 的编程语言之一就是采用 Kotlin/Wasm 形式的 Kotlin。以下列表显示了 Kotlin 团队提供的演示及其源代码。
import kotlinx.browser.document
import kotlinx.dom.appendText
import org.w3c.dom.HTMLDivElement
fun main() {
(document.getElementById("warning") as HTMLDivElement).style.display = "none"
document.body?.appendText("Hello, ${greet()}!")
}
fun greet() = "world"
现在,您可能想知道这有什么意义,因为上面的 Kotlin 代码基本上由转换为 Kotlin 的 JavaScript OM API 组成。与 Compose Multiplatform 结合使用时,它会变得更有意义,因为开发者可以基于他们可能已经为 Android Kotlin 应用创建的界面进行构建。您可以查看 Kotlin/Wasm 图片查看器演示,了解对此功能的早期探索,并探索其源代码(同样由 Kotlin 团队提供)。
Dart 和 Flutter
Google 的 Dart 和 Flutter 团队也正在准备支持 WasmGC。Dart 到 Wasm 的编译工作已接近完成,该团队正在努力提供工具支持,以便将编译为 WebAssembly 的 Flutter Web 应用交付使用。您可以在 Flutter 文档中了解相关工作的当前状态。以下演示是 Flutter WasmGC 预览版。
详细了解 WasmGC
本文仅对 WasmGC 进行了粗略介绍,主要提供了 WasmGC 的概览。如需详细了解此功能,请访问以下链接:
致谢
本文由 Matthias Liedtke、Adam Klein、Joshua Bell、Alon Zakai、Jakob Kummerow、Clemens Backes、Emanuel Ziegler 和 Rachel Andrew 审核。