首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

WasmGC:给GC语言的高效丝滑WebAssembly垃圾回收

在互联网时代,在浏览器中运行新世界成了一个新的目标。为了实现这个目标,之前的JS+Web控件,flash、传统的Web 2.0,甚至html 5技术显然都不够。在这种情况下WebAssembly应运而生。将传统编程语言编译成字节码,然后交给WebAssembly在浏览器中执行,可以实现在浏览器中真真切切的高效、安全的富应用执行。

让传统只能在本地电脑上执行的大型应用程序,比如大型游戏等在浏览器中执行。在WebAssembly的处理和执行中,对于涉及到垃圾回收的GC语言,比如Java、Kotlin、Dart、C#、Golang的处理还不是很周全,由于WebAssembly尽管提供有虚拟机和垃圾收集器,但是其提供的是N+M 而不是N×M。(N种语言+M个虚拟机+G个垃圾收集器),这样就没法高效并行的处理好GC语言执行。考虑到问题,WebAssembly 最新通过的一个GC提案(WasmGC)就是用来解决这个问题,本文我们就一起来学学习一下这个技术。

概述

在将GC语言的执行上传统的方法是通过将该于的实现都编译为WasmMVP(2017年推出的WebAssembly最小可行产品)的方法。

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

这种方法很有意义,因为通常可以为移植到的每个新架构重新编译代码库的主要部分:

在这种方法中,解析器、库支持、垃圾收集器、优化器等都在主运行时的所有架构之间共享。移植到新的架构只需要一个新的后端,代码量相对较少。

在Wasm中这种方法也屡见不鲜。比如Python的移植项目Pyodide和C#的Blazor项目(Blazor支持AOT和JIT编译)。在所有这些情况下,该语言的运行时被编译为WasmMVP,就像编译为Wasm的任何其他程序一样,因此结果使用WasmMVP的线性内存、表、函数等。

WasmMVP方法可以重用几乎所有现有的VM代码,包括语言实现和优化。然而事实证明,这种方法有几个Wasm特有的缺点,所以新的更有效的方法成了需求。

WasmGC方法

简而言之,WebAssembly 的 GC 提案(“WasmGC”)允许定义结构体和数组类型并执行操作,例如创建它们的实例、读取和写入字段、在类型之间进行转换等。这些对象由Wasm VM自己的GC实现来管理,这是该方法与传统移植方法之间的主要区别。

如果将传统的WasmMVP移植方法看做事将一种语言移植到一种架构,那么WasmGC方法与如何将一种语言移植到一个虚拟机非常相似。 例如,如果想将Java移植到JS,那么可以使用像J2CL这样的编译器,它将Java对象表示为JS对象,然后这些JS对象就像所有其他对象一样由JS VM管理。将语言移植到现有VM是一项非常有用的技术,所有编译为JS、JVM 和CLR的语言都已经展示了这一点。

这种架构/虚拟机的比喻并不准确,特别是因为WasmGC的级别比在上一段中提到的其他虚拟机要低。尽管如此,WasmGC定义了VM管理的结构和数组以及用于描述它们的形状和关系的类型系统,并且移植到WasmGC是用这些原语表示语言构造的过程;这肯定比传统的WasmMVP移植(它将所有内容降低到线性内存中的无类型字节)级别更高。因此,WasmGC与语言到虚拟机的移植非常相似,并且它共享此类移植的优点,特别是与目标虚拟机的良好集成以及对其优化的重用。

优势

自动内存管理

在实践中,许多Wasm代码是在已经具有垃圾收集器的虚拟机内运行的(Web上就是这种情况),以及Node.js、workd、Deno和Bun等运行时中的情况。在这些地方,传送GC实现会给Wasm二进制文件增加不必要的大小。事实上,这不仅是WasmMVP中GC语言的问题,也是使用线性内存的语言(如C、C++和Rust)的问题,因为这些语言中执行任何有趣分配的代码最终都会捆绑malloc/free内存管理,这需要几千字节的代码。例如,dlmalloc需要6K,甚至需要以速度换取大小的malloc;还有emmalloc占用超过1K。

而如果让WasmGC让虚拟机自动管理内存,程序根本不需要内存管理代码——既不需要GC,也不需要malloc/free。在有关的测量基准测试中WasmGC比C和Rust都小得多(2.3K对比6.1-9.6K)。

循环垃圾收集

在浏览器中,Wasm经常与JS交互(并通过JS、Web API),但在WasmMVP中,无法在Wasm 和JS之间建立双向链接,从而允许以良好的粒度方式方式收集循环,指向JS对象的链接只能放在Wasm表中,返回Wasm的链接只能将整个Wasm实例作为单个大对象引用,如下所示:

这不足以有效地收集特定的对象周期,其中一些恰好位于已编译的VM中,一些位于JS中。另一方面,使用WasmGC,定义了VM可以识别的Wasm对象,因此可以在Wasm和JS之间进行正确的引用:

堆栈级引用

GC语言必须了解堆栈上的引用,即来自调用范围内的局部变量的引用,因为此类引用可能是保持对象存活的唯一因素。在GC语言的传统移植中,这是一个问题,因为Wasm的沙箱阻止程序检查自己的堆栈。

对于传统端口有一些解决方案,例如影子堆栈,或者仅在堆栈上没有任何内容时收集垃圾(这是在 JS 事件循环之间的情况)。还有未来可能添加的对传统端口有帮助的功能可能是堆栈扫描支持。

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

WasmGC效率

一个相关的问题是执行GC的效率。两种移植方法在这里都有潜在的优势。传统端口可以重用现有虚拟机中针对特定语言定制的优化,例如重点关注优化内部指针或短期对象。

WasmGC端口的优点是可以重用使JS GC更快的所有工作,包括分代GC、 增量收集等技术。WasmGC还将GC留给了VM,这使得诸如高效写屏障之类的事情变得更加简单。

WasmGC 的另一个优点是GC可以意识到内存压力等事情,并可以相应地调整其堆大小和收集频率,就像JS VM 在Web上所做的那样。

内存碎片

随着时间的推移,尤其是在长时间运行的程序中,malloc/free对WasmMVP线性内存的操作可能会导致碎片 想象一下,总共有2 MB内存,而在内存的中间,有一个只有几个字节的现有小分配。在C、C++和Rust等语言中,不可能在运行时移动任意分配,因此在该分配的左侧有近1MB的空间,在右侧有近1MB的空间。 但它们是两个独立的片段,因此如果要尝试分配1.5 MB,就会失败,即使们实际确实有那么多未分配的内存总量:

内存碎片会迫使Wasm模块更频繁地增加内存,这会增加开销并可能导致内存不足错误;这是所有WasmMVP程序中的一个问题,包括GC语言的传统端口(请注意,GC对象本身可能是可移动的,但不是运行时本身的一部分)。

WasmGC方法则完全避免了这个问题,其内存完全由VM管理,VM可以移动它们以压缩GC堆并避免碎片。

开发者工具集成

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

在WasmGC中,VM管理GC对象,因此可以实现更好的集成。例如,在Chrome中,可以使用堆分析器来分析WasmGC程序的内存使用情况:

上图显示了Chrome DevTools 中的“内存”选项卡,其中有一个运行WasmGC代码的页面的堆快照,该代码在链接列表中创建了1001个小对象。可以看到对象类型的名称$Node,和$next,它引用列表中的下一个对象。所有常见的堆快照信息都存在,例如对象数量、浅层大小、保留大小等,让开发者可以轻松查看WasmGC对象实际使用了多少内存。其他Chrome DevTools功能(例如调试器)也适用于WasmGC对象。

问题

语言语义

当在传统端口中重新编译虚拟机时,将获得所需的确切语言,因为正在运行实现该语言的熟悉代码。

而使用WasmGC 端口,最终可能会考虑在语义上做出妥协以换取效率。这是因为通过WasmGC,定义了新的GC类型(结构体和数组)并编译。因此,不能简单地将用C、C++、Rust或类似语言编写的VM编译为其形式,因为它们只能编译到线性内存,因此WasmGC无法帮助绝大多数现有VM代码库。相反,在WasmGC移植中,通常会编写新代码,将语言的构造转换为WasmGC原语。有多种方法可以实现这种转变,但需要进行不同的权衡。

是否需要妥协取决于如何在WasmGC中实现特定语言的构造。例如,WasmGC结构体字段具有固定的索引和类型,因此希望以更动态的方式访问字段的语言可能会面临挑战;有多种方法可以解决这个问题,并且在解决方案的空间中,某些选项可能更简单或更快,但不支持该语言的完整原始语义(WasmGC 目前还存在其他限制,例如,它缺少内部指针;

编译到WasmGC就像编译到现有的VM一样,并且有许多在此类移植中有意义的妥协示例。例如,dart2js(Dart 编译为 JS)数字的行为与DartVM中的不同,IronPython(Python 编译为.NET)字符串的行为类似于C#字符串。因此,并非某种语言的所有程序都可以在此类端口中运行,但做出这些选择是有充分理由的:将dart2js数字实现为JS数字可以让VM很好地优化它们,并且在IronPython中使用.NET字符串意味着可以传递这些字符串到其他.NET代码,而无需任何开销。

虽然WasmGC移植可能需要做出妥协,但与JS相比,WasmGC作为编译器目标也具有一些优势。例如,虽然dart2js具有刚才提到的数字限制,但dart2wasm(Dart编译为WasmGC)的行为完全符合其应有的要求,没有妥协(这是可能的,因为Wasm对Dart所需的数字类型具有有效的表示)。

为什么这对于传统方法来说不是问题?很简单,将现有的虚拟机重新编译为线性内存,其中对象存储在无类型字节中,这比WasmGC级别更低。当拥有的都是无类型字节时,就有更大的灵活性来执行各种低级(并且可能不安全)的技巧,并且通过重新编译现有的虚拟机,还可以获得虚拟机拥有的所有优势。

存在的问题

正如在上一小节中提到的,WasmGC不能简单地重新编译现有的VM。我们也许能够重用某些代码(例如解析器逻辑和 AOT优化,因为它们在运行时不与GC集成),但一般来说,WasmGC需要大量新代码。

相比之下,传统的WasmMVP移植可以更简单、更快,可以在短短几分钟内将Lua VM编译为Wasm。另一方面,Lua的WasmGC则需要做更多的工作,需要编写代码将Lua的构造降低为WasmGC结构和数组,并且需要决定如何在特定的约束内实际执行此操作WasmGC类型系统。

因此,需要更多的工具链工作是WasmGC移植的一个显著缺点。然而,考虑到之前提到的所有优点,WasmGC仍然非常有吸引力。理想的情况是WasmGC的类型系统可以有效地支持所有语言,并且所有语言都致力于实现WasmGC目标。

第一部分将有需要WasmGC类型系统的未来添加。

第二部分可以通过尽可能分担工具链方面的工作来减少WasmGC移植所涉及的工作。

幸运的是,事实证明WasmGC使得共享工具链工作变得非常实用。

优化

我们已经提到过WasmGC端口具有潜在的速度优势,例如使用更少的内存和在主机 GC 中重用优化。在本节中,我们将展示WasmGC相对于WasmMVP的其他有趣的优化优势,这些优势会对WasmGC的设计方式以及最终结果的速度产生很大影响。

优化流程对比

关键问题是WasmGC比WasmMVP级别更高。先比较传统WasmMVP方法是移植到新架构,而WasmGC方法则是移植到新的VM,而VM当然是架构的更高级别抽象——而且更高层次的表示通常更容易优化。

具体代码演示:

func foo() {let x = allocate(); // 分配一个GC对象x.val = 10; // 给var属性赋值为10let y = allocate(); // 分配另外一个对象y.val = x.val; // 这个必须是10return y.val; // 函数返回10}

上述代码中, x.val将包含10,正如将返回y.val,所以最终的返回是10一样,优化器甚至可以删除分配的过程,直接为:

func foo() {return 10;}

然而,这样的优化在WasmMVP中是不可能的,因为每次分配都会变成对malloc,Wasm中的一个大而复杂的函数,对线性内存有副作用。由于这些副作用,优化器必须假设第二次分配(对于y)可能会改变x.val,它也驻留在线性存储器中。内存管理很复杂,在Wasm中以较低的级别实现它时,优化选项就会受到限制。

相比之下,在WasmGC中可在更高的级别上操作:每次分配都会执行 struct.new指令,实际上可以推理的虚拟机操作,优化器也可以跟踪引用来得出结论:x.val值只被写入一次10。 因此,可以将该函数优化为简单的返回值 10。

除了分配之外,WasmGC添加的其他内容是显式函数指针(ref.func)并使用它们进行调用(call_ref)、结构体和数组字段的类型(与无类型线性内存不同)等等。 因此,WasmGC是比WasmMVP更高级别的中间表示(IR),并且可优化性更高。

如果WasmMVP 的可优化性有限,为什么它能这么快?毕竟,Wasm的运行速度非常接近原生速度。这是因为WasmMVP通常是LLVM等强大优化编译器的输出。LLVM IR与WasmGC类似,但与WasmMVP不同,它对分配等有特殊的表示,因此LLVM可以优化我们一直在讨论的事情。工具链级别WasmMVP的设计是,大多数优化发生在Wasm之前的,而Wasm虚拟机只做“最后一公里”的优化(例如寄存器分配)。

WasmGC 是否可以采用与WasmMVP类似的工具链模型,特别是使用LLVM? 不幸的是,不,因为LLVM不支持 WasmGC。 此外,许多GC语言不使用LLVM——该领域有各种各样的编译器工具链。所以需要为WasmGC做一些其他的事情。

WasmMVP和WasmGC工作流程都从左侧相同的两个框开始:从以特定于语言的方式(每种语言最了解自己)进行处理和优化的源代码开始。那么就会出现一个区别:

对于WasmMVP,必须先进行通用优化,然后再降低到Wasm,而对于WasmGC,可以选择先降低到Wasm,然后再优化。这很重要,因为降低后进行优化有很大的优势:然后可以在编译为WasmGC的所有语言之间共享工具链代码以进行通用优化。下图显示其过程:

由于可以在编译到WasmGC后再进行一般优化,因此Wasm-to-Wasm优化器可以帮助所有WasmGC编译器工具链。

工具链优化

Binaryen WebAssembly工具链优化器项目已经对 WasmMVP 内容进行了广泛的优化,例如内联、常量传播、死代码消除等,几乎所有这些也适用于WasmGC。 然而,正如之前提到的,WasmGC可以实现比WasmMVP更多的优化相应地编写了很多新的优化:

逃逸分析将堆分配移至本地。

去虚拟化将间接调用转变为直接调用。

更强大的全局死代码消除。

全程序类型感知内容流分析(GUFA)。

Actor优化,例如删除多余的Actor并将其移至较早的位置。

类型修剪、合并和精炼(对locals、globals、 fields和签名)

为了衡量Binaryen 中所有这些优化的有效性,对比没有wasm-opt编译器的Java性能输出,将Java编译WasmGC:

没有wasm-opt”意味着不运行Binaryen的优化,但仍然在VM和J2Wasm编译器中进行优化。如图所示,wasm-opt对每个基准测试都提供了显着的加速,平均速度提高了1.9倍

总之,wasm-opt可以被任何编译为 WasmGC的工具链使用,并且避免了在每个工具链中重新实现通用优化的需要。而且,随着不断改进Binaryen的优化,这将使所有使用的工具链受益wasm-opt,就像LLVM的改进可以帮助所有使用LLVM编译为WasmMVP的语言一样。

工具链优化只是其中的一部分,Wasm虚拟机的优化也绝对至关重要。

V8 优化

前面说了WasmGC比WasmMVP更可优化,不仅工具链可以从中受益,虚拟机也可以从中受益。事实证明这很重要,因为GC语言与编译为WasmMVP的语言不同。 例如,考虑内联,这是最重要的优化之一:C、C++和Rust等语言在编译时内联,而Java和Dart等GC语言通常在运行时内联和优化的VM中运行。这种性能模型影响了语言设计以及人们用GC语言编写代码的方式。

例如,在像Java这样的语言中,所有调用都是以间接方式开始的(子类可以重写父函数,即使使用父类型的引用调用子类时也是如此)。每当工具链可以将间接调用转变为直接调用时,都会受益,但实际上,现实世界中的Java程序中的代码模式通常具有实际上确实有大量间接调用的路径,或者至少是无法静态推断为直接调用的路径。为了很好地处理这些情况,在V8中实现了推测内联,也就是说,间接调用在运行时发生时会被记录下来,如果发现调用站点具有相当简单的行为(很少有调用目标),那么就会通过适当的保护检查,这比将这些事情完全留给工具链更接近Java通常的优化方式。

现实世界的数据验证了这种方法。根据测量Google Sheets Calc Engine的性能,该引擎是一个用于计算电子表格公式的Java代码库,已经用J2CL将被编译为JS。V8团队一直在与Sheets和J2CL合作,将该代码移植到WasmGC,这既是因为Sheets的预期性能优势,也是为了为WasmGC规范流程提供有用的实际反馈。从性能来看,推测内联是在V8中为WasmGC实现的最重要的单独优化,如下图所示:

其他选项是指除了推测内联之外的优化,可以出于测量目的而禁用这些优化,其中包括:负载消除、基于类型的优化、分支消除、常量折叠、转义分析和公共子表达式消除。“No opts”表示已经关闭了所有这些以及推测内联(但 V8 中存在其他优化,我们无法轻易关闭;因此,这里的数字只是一个近似值)。 投机内联带来的巨大改进(约30% 的加速,表明内联至少在已编译的Java上有很优秀的表现。

除了推测内联之外,WasmGC还构建在V8中现有的Wasm支持之上,这意味着它受益于相同的优化器管道、寄存器分配、分层等。除此之外,WasmGC的特定方面可以受益于额外的优化,其中最明显的是优化WasmGC提供的新指令,例如高效实现类型转换。另一项重要工作是在优化器中使用WasmGC的类型信息。例如,ref.test在运行时检查引用是否属于特定类型,在检查成功后ref.cast,对同一类型的强制转换也必须成功。这有助于优化Java中的如下模式:

if (ref instanceof Type) {foo((Type) ref);}

这些优化在推测内联之后特别有用,因为就能看到比工具链在生成Wasm时看到的更多内容。

总的来说,在WasmMVP中,工具链和虚拟机优化之间有相当清晰的分离:在工具链中做了尽可能多的事情,只为虚拟机留下了必要的部分,这是有意义的,因为它使虚拟机变得更简单。使用WasmGC,这种平衡可能会有所改变,因为正如我们所看到的,需要在运行时对GC语言进行更多优化,而且WasmGC本身也更加可优化,可在工具链和虚拟机优化之间有更多的重叠。

试用

目前,WasmGC已经通过W3C的4阶段审核,成为一个完整的正式标准,在Chrome 119中有了其支持。使用该浏览器(或任何其他具有WasmGC支持的浏览器;例如,稍后晚些时候推出的火狐120也会推出WasmGC支持)。

可以在支持的浏览器中运行Flutter演示,其中编译为WasmGC的Dart驱动应用程序的逻辑,包括其小部件,布局和动画。

总结

WasmGC是一种在WebAssembly中实现GC语言的全新的、高效、安全的、有前途的方法。

WasmGC比能够比传统方法更小,甚至比用C、C++或Rust编写的WasmMVP 程序还小,它们在循环收集、内存使用、开发人员工具等方面与Web集成得更好。 WasmGC也是一种更优化的表示形式,它可以提供显著的速度优势以及在更易于在语言之间实现工具链共享。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Ogn0FelnqH_Mv_oPcjfhFe5A0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com