WasmGC发布!浏览器运行Python/Java/C只是时间问题?

前有科技后进阶 2024-04-14 02:53:25

大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

注意:本文部分内容来自于 Alon Zakai 发表的文章,但是对部分内容进行了润色和修改,文章链接参考末尾。

什么是垃圾回收

目前有两种类型的编程语言,即:自动垃圾收集的编程语言(如:Kotlin、PHP 或 Java、Python)和需要手动内存管理的编程语言( C、C++ 或 Rust)。

垃圾收集(Garbage Collection,即 GC)的主要思想是尝试回收由程序分配但不再被引用的内存。 实现垃圾收集的策略有很多,比如:引用计数,其目标是计算内存中对象的引用数量,当不再有对对象的引用时可以将其标记为不再使用,从而准备好进行垃圾回收。

1. 将 GC 语言有效引入 WebAssembly 的两种方法

很多编程语言是用其他编程语言实现的,例如:PHP 运行时主要是用 C 实现的。可以在 GitHub 上查看 PHP 源代码,垃圾收集代码位于文件 zend_gc.c 中。

大多数开发人员会通过其操作系统的包管理器安装 PHP,但开发人员也可以从源代码构建 PHP。 例如,在 Linux 环境中,执行 ./buildconf && ./configure && make 将为 Linux 运行时构建 PHP。 但也意味着 PHP 运行时可编译为其他运行时 (Runtime),比如:Wasm。

1.1 将语言移植到 Wasm 运行时的传统方法

假如 Python 想要运行在 ARM 架构上,或者 Dart 想要运行在 MIPS 架构上 (X86、ARM、RISC-V、MIPS 是目前四大主流芯片架构),总体思路是将虚拟机重新编译为该目标架构。 如果虚拟机具有特定于架构体系的代码,例如:JIT(just-in-time) 或 AOT(ahead-of-time) ,那么开发者还需要为新体系架构实现 JIT/AOT 后端。 但是,这种方法很有意义,因为通常可以为移植到的每个新架构重新编译代码库的核心部分。

在上图中,解析器(Parser)、库支持 (Library Support)、垃圾收集器 (Garbage Collector)、优化器 (Optimizer) 等都在主运行时的所有架构之间共享,而移植到新的架构只需要一个新的后端,相比之下代码量很少。

Wasm 是一个底层编译器目标,因此也可使用传统的移植方法。比如:自从 Wasm 首次启动以来,用于 Python 的 Pyodide 和用于 C# 的 Blazor(注意,Blazor 支持 AOT 和 JIT 编译,因此是很好的示例)都在实践中得到很好的证明。 在这些情况下,语言的运行时和其他语言一样都被编译为 WasmMVP(Minimal Viable Product),因此也就可以使用 WasmMVP 的线性内存、表、函数等。

这种对这种方法的缺点和优点简单做下总结:

优点:可以重用几乎所有现有的 VM 代码,包括:语言本身的实现和优化缺点:某些语言有自己的垃圾收集器,如:Python 等,其最终会编译成 Wasm 在浏览器中运行,此时会和 V8 自身的垃圾收集器同时存在,从而产生浪费1.2 使用 WasmGC 将编程语言移植到新的运行时

当前的 Wasm MVP 只能处理线性内存中的数字,即: 整数和浮点数,随着引用类型 (externref) 提案的发布,Wasm 还可以保留外部引用。

WasmGC (WebAssembly Garbage Collection) 提案允许定义结构体(Struct)和数组类型(Array Heap)并执行操作,例如:创建实例、读取和写入字段、在类型之间进行转换等。 这些对象由 Wasm VM 的 GC 实现来管理,这是与传统移植方法间的核心区别。

如果将传统的移植方法理解为将一种语言移植到一种架构,那么 WasmGC 更像是将一种语言移植到一个虚拟机。WasmGC 定义了 VM 能管理的结构和数组以及用于描述其形状和关系的类型系统,移植到 WasmGC 是用这些原语表示语言构造的过程。

例如:如果想将 Java 移植到 JavaScript,可以使用 J2CL 编译器,其将 Java 对象表示为 JavaScript 对象,然后 JavaScript 对象就像所有其他对象一样由 JavaScript VM 管理。

总之,WasmGC 提案通过结构体 (Struct) 和数组堆 (Array Heap) 类型为 WebAssembly 增加了对高级语言的有效支持,使针对 Wasm 的语言编译器能够与主机 VM 中的垃圾收集器集成。 这也意味着使用 WasmGC 将编程语言移植到 Wasm 时垃圾收集器本身不再需要一起移植,即可以使用现有 VM 的垃圾收集器。

为了验证这一改进的实际影响,Chrome 的 Wasm 团队编译了来自 C、Rust 和 Java 的 Fannkuch 基准测试版本(在工作时分配数据结构)。

C 和 Rust: 二进制文件可能在 6.1 K 到 9.6 K 之间,具体取决于各种编译器标志Java: 体积小得多,只有 2.3 K!

C 和 Rust 不包含垃圾收集器,但其仍然打包 malloc/free 来管理内存,而 Java 较小的原因是其根本不需要打包任何内存管理代码。

2. 传统移植和 WasmGC 的不同2.1 WasmGC 无需移植内存管理代码

在 Web 或者服务端运行时(如:Node.js、Deno 和 Bun )上,Wasm 代码是在已经具有垃圾收集器的 JavaScript 虚拟机内运行的。 此时,移植 GC 会给增加 Wasm 二进制文件不必要的大小。

事实上,这不仅是 WasmMVP 中 GC 语言的问题,也是使用线性内存语言(如 C、C++ 和 Rust)的问题,因为这些语言中执行任何分配的代码最终都会将 malloc/free 打包到管理线性内存,这需要几千字节的代码。

dlmalloc:6k+emmalloc:速度换大小,1k+

而 WasmGC 让虚拟机自动管理内存,因此 Wasm 中根本不需要内存管理代码,既不需要 GC,也就不需要 malloc/free。

2.2 WasmGC 解决循环收集 (Cycle Collection)

在浏览器中,Wasm 经常与 JavaScript 交互( JavaScript、Web API),但在 WasmMVP(即使引用类型提案)中,无法在 Wasm 和 JS 之间建立双向链接以允许更好的收集循环。

const importObject = {imports: { imported_func: (arg) => console.log(arg) } };WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then( (obj) => obj.instance.exports.exported_func(),);

在这种情况下,指向 JS 对象的链接只能放在 Wasm 表中,返回 Wasm 的链接只能将整个 Wasm 实例作为单个大对象引用,如下所示:

这不足以有效收集特定的对象周期,因为一些对象位于已编译的 VM 中,一些位于 JavaScript 中。 而 WasmGC 通过定义了 VM 可以识别的 Wasm 对象,因此可以在 Wasm 和 JavaScript 之间进行正确的引用。

2.3 WasmGC 解决堆栈上的 GC 引用

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

对于传统方案也有一些解决方案,例如:影子堆栈(可以自动完成),或者仅在堆栈上没有任何内容时收集垃圾(这是在 JavaScript 事件循环之间的情况)。 未来也可能添加对传统方式有帮助的功能,比如:堆栈扫描支持 (Stack Scanning Support)。

目前,WasmGC 可以做到完全自动、无开销地处理堆栈引用因为 Wasm VM 负责 GC。

2.4 GC Efficiency

一个相关的问题是执行 GC 的效率,两种移植方法在这里都有潜在的优势。

传统方法可以重用现有虚拟机中针对特定语言定制的优化,例如:重点关注优化内部指针或短期对象。 而在 Web 上运行的 WasmGC 方法的优点是可以重用使 JavaScript GC 更快的所有工作,包括:分代 GC、增量收集等技术。WasmGC 还将 GC 留给了 VM, 这使得诸如高效写屏障之类的事情变得更加简单。

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

2.5 内存碎片

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

这种碎片会迫使 Wasm 模块更频繁地增加内存,从而导致潜在的内存不足错误。 这也是所有 WasmMVP 程序中的一个问题,包括: GC 语言的传统方案(请注意,GC 对象本身可能是可移动的,但不是运行时本身的一部分)。 而 WasmGC 有效避免了这个问题,因为内存完全由 VM 管理,VM 可以移动以压缩 GC 堆并避免碎片。

2.6 其他2.6.1 Chrome 工具支持

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

而在 WasmGC 中,VM 负责管理 GC 对象,因此可以实现更好的集成。 例如,在 Chrome 中,可以使用堆分析器 (heap profiler) 来测量 WasmGC 程序的内存使用情况。

2.6.2 语言熟悉

当采用重新编译虚拟机的方式时,开发者对一切都非常熟悉,这就是优势! 相比之下,使用 WasmGC 端口,开发者最终可能会考虑在语义上做出妥协以换取效率。

这是因为 WasmGC 相当于定义了新的 GC 类型(结构体和数组)作为编译目标。 因此,不能简单地将用 C、C++、Rust 或类似语言编写的 VM 编译为该形式,而只能编译到线性内存。即,WasmGC 无法最大限度利用现有 VM 代码库 。

这也意味着,在 WasmGC 移植中,开发者通常会编写新代码,将语言的构造转换为 WasmGC 原语。而是否妥协取决于特定语言的构造 (Construct) 如何在 WasmGC 中实现。

参考资料

https://developer.chrome.com/blog/wasmgc

https://v8.dev/blog/wasm-gc-porting

https://webassembly.org/features/

https://github.com/WebAssembly/gc/blob/main/proposals/gc/MVP.md

0 阅读:0

前有科技后进阶

简介:感谢大家的关注