将GC编程语言引入WebAssembly的新方法


本文讨论了一种名为 WasmGC 的新方法,用于将垃圾收集编程语言有效地引入 WebAssembly。

WasmGC 定义了新的 GC 类型,例如结构和数组,与之前编译为线性内存的方法 (WasmMVP) 相比,它们可以实现更好的优化:

  • 在编译时和运行时进行优化,例如间接调用的推测内联。性能测量结果表明,推测性内联可使编译后的 Java 代码显著提速 30%。
  • WasmGC 比 WasmMVP 更可优化,因为它在更高级别上运行,跟踪对象引用。
  • 虽然 WasmGC 可能需要在某些语言的语义上做出妥协,但它可以提供更小的代码大小和更好的性能等好处。
  • WasmGC 现在已成为一种标准,并在浏览器中得到支持,从而实现了在 WebAssembly 中实现语言的新方法,并且具有比传统编译器方法更好的性能潜力。

Java、Kotlin、Dart、Python 和 C# 等 GC 语言移植到 Wasm 主要有两种方法:

  • WasmMVP:“传统”移植方法,即将该语言的现有实现编译为 WasmMVP,即 2017 年推出的 WebAssembly 最小可行产品。
  • WasmGC:其中语言被编译为 Wasm 本身的 GC 构造,这些构造在最近的 GC 提案中定义。

WasmMVP通常如何移植到新硬件架构?
比如说Python想要运行在ARM架构上,或者Dart想要运行在MIPS架构上。总体思路是将虚拟机重新编译为该架构。
除此之外,如果虚拟机具有特定于体系结构的代码,例如即时 (JIT) 或提前 (AOT) 编译,那么您还可以为新体系结构实现 JIT/AOT 后端。

Wasm 是一个低级编译器目标,因此可以使用传统的移植方法也就不足为奇了。自从 Wasm 首次启动以来,我们在许多情况下都在实践中看到了这种效果,例如用于 Python 的 Pyodide用于 C# 的 Blazor(请注意,Blazor 支持AOTJIT编译,因此它是上述所有内容的一个很好的示例)。

在所有这些情况下,该语言的运行时被编译为 WasmMVP,就像编译为 Wasm 的任何其他程序一样,因此结果使用 WasmMVP 的线性内存、表、函数等。它很有意义,因为您可以重用几乎所有现有的 VM 代码,包括语言实现和优化。

然而事实证明,这种方法有几个 Wasm 特有的缺点,而这正是 WasmGC 可以提供帮助的地方。

WasmGC 移植新方法
简而言之,WebAssembly 的 GC 提案(“WasmGC”)允许您定义结构体和数组类型并执行操作,例如创建它们的实例、读取和写入字段、在类型之间进行转换等。(有关更多详细信息,请参阅提案概述)。

这些对象由 Wasm VM 自己的 GC 实现来管理,这是该方法与传统移植方法之间的主要区别。

如果传统的移植方法是将一种语言移植到一种架构,那么 WasmGC 方法与如何将一种语言移植到一个虚拟机非常相似。
例如,如果您想将 Java 移植到 JavaScript,那么您可以使用像J2CL这样的编译器,它将 Java 对象表示为 JavaScript 对象,然后这些 JavaScript 对象就像所有其他对象一样由 JavaScript VM 管理。

现在我们已经了解了 GC 语言的两种移植方法是什么,让我们看看它们如何比较。

运送内存管理代码
WasmGC 让虚拟机自动为我们管理内存,因此我们在Wasm 中根本不需要malloc/free内存管理代码,既不需要 GC,也不需要内存管理代码。

循环收集
在浏览器中,Wasm 经常与 JavaScript 进行交互(并通过 JavaScript 与 Web API 进行交互),但在 WasmMVP 中(甚至在引用类型提案中),Wasm 和 JS 之间无法建立双向链接,从而无法以细粒度的方式收集循环。指向 JS 对象的链接只能放在 Wasm 表中,而指向 Wasm 的链接只能将整个 Wasm 实例作为一个单独的大对象来引用。这不足以有效地收集特定的对象周期,其中一些恰好位于已编译的 VM 中,一些位于 JavaScript 中。

而使用 WasmGC,我们定义了 VM 可以识别的 Wasm 对象,因此我们可以在 Wasm 和 JavaScript 之间进行正确的引用。

堆栈上的 GC 引用
目前,只有 WasmGC 可以无开销地处理堆栈引用,并且由于 Wasm VM 负责 GC,因此它完全自动执行此操作。

GC效率
一个相关的问题是执行 GC 的效率。在这方面,两种移植方法都有潜在的优势。传统的移植方式可以重复使用现有虚拟机中的优化功能,这些优化功能可能是为特定语言量身定做的,例如重点优化内部指针或短寿命对象。

另一方面,在Web上运行的 WasmGC 移植的优势在于,它可以重复使用所有为使 JavaScript GC 快速而做的工作,包括生成 GC、增量收集等技术。WasmGC 还将 GC 交给虚拟机处理,从而简化了高效写入障碍等工作。

WasmGC 的另一个优势是,GC 可以感知内存压力等情况,并相应地调整堆大小和收集频率,这与 JavaScript 虚拟机在网络上的做法如出一辙。

内存碎片
随着时间的推移,特别是在长期运行的程序中,对 WasmMVP 线性内存进行 malloc/free 操作可能会导致内存碎片。

这种碎片化会迫使 Wasm 模块更频繁地增长内存,从而增加开销,并可能导致内存不足错误;目前正在设计改进措施,但这是一个具有挑战性的问题。这是所有 WasmMVP 程序都会遇到的问题,包括传统的 GC 语言移植(注意,GC 对象本身可能是可移动的,但运行时本身的部分却不是)。

而 WasmGC 则避免了这一问题,因为内存完全由虚拟机管理,虚拟机可以移动它们来压缩 GC 堆,避免碎片化。

开发工具集成
在传统的 WasmMVP 移植中,对象被放置在线性内存中,开发工具很难提供有用的信息,因为这些工具只能看到字节,而没有高级类型信息。

而在 WasmGC 中,虚拟机管理 GC 对象,因此可以更好地集成。例如,在 Chrome 浏览器中,您可以使用堆剖析器来测量 WasmGC 程序的内存使用情况

语言语义
在 WasmGC 中,我们定义了新的 GC 类型--结构体和数组--并编译成它们。因此,我们不能简单地将用 C、C++、Rust 或类似语言编写的虚拟机编译成这种形式,因为这些语言只能编译成线性内存,所以 WasmGC 无法帮助绝大多数现有的虚拟机代码库。在 WasmGC 移植中,您通常需要编写新代码,将您的语言结构转换为 WasmGC 基元。