现在,Chrome 会默认启用 WebAssembly 垃圾回收 (WasmGC)

编程语言分为两类:垃圾回收型编程语言和需要手动内存管理的编程语言。前者的示例包括 Kotlin、PHP 或 Java 等众多语言。后者的示例包括 C、C++ 或 Rust。一般而言,高级编程语言更有可能将垃圾回收作为一项标准功能。本文将重点介绍此类垃圾回收型编程语言以及如何将其编译为 WebAssembly (Wasm)。但首先,什么是垃圾回收(通常称为 GC)?

浏览器支持

  • Chrome:119.
  • Edge:119.
  • Firefox:120.
  • Safari:不受支持。

垃圾回收

简而言之,垃圾回收的概念是指尝试回收由程序分配但不再引用的内存。这种内存称为垃圾。实现垃圾回收的方法有很多。其中之一是引用计数,其目标是统计内存中对象的引用数量。当没有对某个对象的更多引用时,该对象可以被标记为不再使用,从而准备好进行垃圾回收。PHP 的垃圾回收器使用引用计数,您可以使用 Xdebug 扩展程序的 xdebug_debug_zval() 函数来了解其内部运作方式。请考虑以下 PHP 程序。

<?php
  $a= (string) rand();
  $c = $b = $a;
  $b = 42;
  unset($c);
  $a = null;
?>

该程序会将转换为字符串的随机数分配给一个名为 a 的新变量。然后,它会创建两个新变量(bc),并为其赋值 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 等动态语言那样存在优化降级的风险。因此,该提案通过结构体和数组堆类型为 WebAssembly 添加了对高级管理语言的高效支持,这些类型可让以 Wasm 为目标平台的语言编译器与主机虚拟机中的垃圾回收器集成。简而言之,这意味着,使用 WasmGC 将编程语言移植到 Wasm 意味着编程语言的垃圾回收器不再需要包含在移植中,而是可以使用现有的垃圾回收器。

为了验证这项改进在实际中的效果,Chrome 的 Wasm 团队使用 CRustJava 编译了 Fannkuch 基准测试(该测试会在运行时分配数据结构)。C 和 Rust 二进制文件的大小可能介于 6.1 K9.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 LiedtkeAdam KleinJoshua BellAlon ZakaiJakob KummerowClemens BackesEmanuel ZieglerRachel Andrew 审核。