译者 | 朱先忠
策划 | 云昭
WebAssembly已经存在了一段时间,但到目前为止,它对高级语言的实用性有限,尤其是使用垃圾收集的语言。然而,随着web浏览器即将推出对托管内存的支持,情况即将发生变化,这使得WebAssembly成为Scheme、OCaml以及所有非C++或Rust使用者的可行目标。
本文将回顾为什么1.0版本的WebAssembly不是Scheme的好目标,变通方法是什么,新设施是什么,实现将如何利用它们,以及仍然存在哪些限制。在2-3年内,WebAssembly将成为我们许多最熟悉的语言的优秀编译目标和语言运行时基础。
WebAssembly,终于要来了。
1、现实中的WebAssembly
WebAssembly可以看作是C编译器的一个奇怪的后端。目前,只有少数几种源语言能够成功运行在WebAssembly上。
Haskell、Ocaml、Scheme、F#等语言呢?我们呢?我们只算是一些懒虫吗?
那我们为什么不在那里呢?WebAssembly上的Clojure在哪里?F#、Elixir和Haskell编译器在哪里?人们早期的确作出一些努力,但并没有真正成功。为什么?我们只是没有付出努力吗?为什么Rust语言可以飞速发展,而Scheme语言却没有呢?
WebAssembly的1.0和2.0版本还不能算是良好的支持垃圾收集的语言,让我们仔细看看其原因。
事实证明,在WebAssembly上尚不存在好的Scheme语言实现是有原因的:如果您的语言依赖于存在垃圾收集器的话,那么WebAssembly的初始版本就是一个可怕的目标。尽管已经取得了一些进展,但对于当前标准化和部署的WebAssembly版本仍然有待观察。为了更好地理解这个问题,让我们深入研究这个系统的内部,看看它的局限性是什么。
2、GC和WebAssembly 1.0
基于垃圾收集(GC:Gabage Collecting)的值类型存储在什么地方呢?
对于WebAssembly 1.0而言,唯一可能的答案是:线性内存。
WebAssembly 1.0为您提供的表示数据的原型是所谓的线性内存:其实,也就是一个可以读取和写入的字节缓冲区。除了内存布局更简单之外,这与本机编译时得到的非常相似。您可以以64 KB的页面为单位获取此内存。在上面的例子中,我们将请求10个页面,640kB。应该足够了,对吧?我们将把它全部用于垃圾收集器,并使用一个凹凸指针分配器。堆指针/分配指针保存在可变全局变量$hp中。
这里给出了分配函数的样子。分配函数$alloc类似于C语言中的malloc函数:它使用一些字节作参数并返回一个指针。在WebAssembly中,指向内存的指针只是一个偏移量,它是一个32位整数(i32)。(使用64位地址空间的选项已经纳入计划中,但还不是标准的实现。)
如果这是您第一次看到WebAssembly函数的文本表示,那么您可以尽情地学习一下,但这却不是我上面演示内容的重点;我想强调的是call $gc——当分配指针到达区域的末尾时所发生的事情。
3、GC和WebAssembly 1.0(1)
调用$gc背后隐藏着什么?答案是:通过线性内存运送GC。基于“Stop-The-World”技术,采取非并行、非并发机制,而是……根节点。
首先要注意的是,您必须自己提供$gc。当然,这是可行的——这就是我们在编译到本地目标时所要做的工作。
不幸的是,尽管WebAssembly中的多线程支持有些不足;不过,它允许您共享内存并使用原子操作,只是您必须在WebAssembly之外创建线程。在开发实践中,您交付的GC可能没有利用多线程优势,因此它可能是相当原生态的,以致于将所有垃圾收集工作推迟到“Stop-The-World”阶段。
4、GC和WebAssembly 1.0(2)
活动对象包括:
- 根(roots)节点
- 活动对象引用的任何对象
其中,根(roots)节点是活动堆栈帧中的全局值与局部值。
注意:你无法通过编程来访问活动堆栈帧。
更糟糕的是,您无法访问堆栈上的根(roots)节点。一个GC必须保留活动对象——它们是被循环定义为根引用的任何对象或活动对象引用的任何物体。从根节点开始,作为全局变量或者是被活动堆栈框架引用的任何GC托管对象。
但是,这种情况下我们遇到了问题,因为在WebAssembly(任何版本,而不仅仅是1.0版本)中,你不能遍历堆栈,所以你根本找不到活动的堆栈帧;当然,这样一来你也找不到堆栈根节点。(有时,人们希望将其作为一种低级别的能力来支持;但一般来说,最后形成的共识似乎是,如果由引擎来负责实现GC机制,那么系统的整体性能会更好一些;但目前这仅是一个预兆而已!)
5、GC和WebAssembly 1.0(3)
针对上述问题,一种变通的解决方案是:
- 精确控制堆栈的根节点
- 将所有可能的指针值溢出到线性内存并保守地收集
考虑到堆栈的不可迭代性,基本上有两种变通的解决方案。一种是让编译器和运行时维护对象根节点的显式堆栈;这样一来,垃圾收集器可以确定这些根节点是指针。这种办法很好,因为它可以让你移动物体。但是,维护堆栈也是一笔开销;基于现有技术的解决方案是创建一个辅助表(“堆栈映射”),将可以调用GC的每个潜在点与如何找到根节点的指令相关联。
另一种解决方法是将整个堆栈溢出到内存中。或者,可能只是类似指针的值;无论如何,需要保守地扫描所有关键词,以便搜索到可能是根节点的东西。但是,采用这种方案的话需要我们必须自己访问内存,而不是访问WebAssembly实现会溢出堆栈到达的内存区。这种方案可能还凑合;但它算是次优的。有兴趣的读者可以参阅我最近关于Whippet垃圾收集器的帖子(https://wingolog.org/archives/2023/02/07/whippet-towards-a-new-local-maximum),以深入了解基于保守性根节点搜索的含义。
6、GC和WebAssembly 1.0(4)
· 无法收集外部对象(如JavaScript)的循环。
· 指向GC托管对象的指针是对线性内存的偏移,需要在线性内存上具有从外部·读取/写入对象的能力。
· 无法将内存回馈给操作系统。
· 想进行详细的“肠道检查”:它的回答是“NO”。
如果仅此而已,情况就不那么好了,而且情况会变得更糟!线性内存GC的另一个问题是,它限制了将多个模块和主机组合在一起的可能性,因为在Web浏览器中管理JavaScript对象的垃圾收集器对线性内存上的垃圾收集器一无所知。在这样的系统中,您可以很容易地创建内存泄漏。
此外,为了读取或写入对象的字段,对线性内存中对象的引用需要对所有线性内存进行任意读写访问,这一点非常令人讨厌。那么,如何在适当修改的情况下构建一个可靠的系统呢?
最后,一旦你收集了垃圾,也许你设法压缩了内存,你就不能返回给操作系统任何东西了。对于这点,已经有一些建议正在酝酿中,但还没有实现。
如果BOB的观众必须在“更糟糕的是更好的”和“正确的事情”之间做出选择的话,我认为BOB的观众更接近于选择“正确的事情”。像这样的观众本能地会对丑陋的系统感到厌恶;我认为GC相对于线性存储差不多也描述了一个丑陋的系统。
7、GC和WebAssembly 1.0(5)
浏览器中已经存在一个高性能并发并行压缩的GC了。
关键是,WebAssembly 1.0要求您编写和交付一个糟糕的GC,而主机中可能已经有一个很棒的GC——一个投入了数百人年努力的GC,一个肯定会做得比你做得更好的GC。web浏览器中托管的WebAssembly应该可以访问浏览器的垃圾收集器!
我有一种感觉,当我们这些对垃圾收集语言情有独钟的人,一直站旁观席上时,Rust和C++程序员们却一直忙于“在球场上进球”。是的,他们被“球”绊倒了,但最终他们还是设法在击“球”距离内成功了。
8、变革即将到来!
对内置GC的支持将于2023年第四季度推出。
有了GC,基本条件已经到位。
让我们将语言编译到WebAssembly。
为了继续使用体育环境下“足球”的比喻,我认为在下半场,我们的球员将最终能够上场,并达到众所周知的110%。WebAssembly用户开始支持垃圾收集,我认为即使到今年年底,它也将在主要浏览器中推出。这将是一个大事件!我们有机会,我们需要好好把握。
当然,正如我前面所提到的,WebAssembly仍然是一台奇怪的机器:作为编译目标有些奇怪,在运行时也是如此。对于调试支持方面,简直是一场恰到好处的混乱;也许过段时间会有其他关于这方面的文章。
对于如何表示字符串,这是一个令人惊讶的棘手问题;在WebAssembly标准社区中,有人认为JavaScript和WebAssembly可以共享底层字符串表示,也有人认为这是一件愚蠢的事,而认为复制是唯一的出路。我不知道哪一方会获胜;也许稍后也会有更多关于这方面的内容。
类似地,与JavaScript的整个互操作问题在很大程度上处于早期阶段,目前的情况是选择什么都不做而不是做错事。您可以将WebAssembly(ref-eq)传递给JavaScript,但JavaScript对它无能为力:它没有原型。现有技术还提供了一个JS运行时,它封装了每个wasm对象,将wasm模块中导出的函数代理为对象方法。
最后,一些语言实现确实需要JIT支持,比如PyPy。
9、总结
有了GC支持,WebAssembly现在已经为我们准备好了。
把我们熟悉的语言放在WebAssembly上现在已经是一件很容易的事情了。
那么,让我们在下半场进球吧!
请访问以下有关链接:
- "gitlab.com/spritely/guile-hoot-updates"
- "wingolog.org"
- "wingo@igalia.com"
- "igalia.com"
- "mastodon.social/@wingo"
事实证明,WebAssembly在C、C++、Rust等方面取得了一些巨大的胜利,但现在轮到我们参与到游戏中了。GC即将到来,作为一个社区,我们需要准备好编译器和语言运行时。让我们冲好咖啡,把一些字节放在一起;现在还为时过早,不过,对于拥有最佳WebAssembly体验的语言社区来说,这是一个值得赢得的世界。
10、WebAssembly是一个令人兴奋的新型通用计算平台
WebAssembly到底是什么?它不是一种你用于编写软件的编程语言,而是一种编译目标:如果你愿意学习的话,它基本算是一种汇编语言。
对于此平台,它拥有可预测的便携性能:
- 低层级上运行
- 本地代码约占不到10%
通过隔离实现可靠的组合:
- 默认情况下,模块不共享任何内容
- 没有梦魇般错误
- 提供内存沙盒支持
你需要将代码编译到WebAssembly,以便更轻松地进行部署和创作。
如果你把WebAssembly的特点看作一台抽象机器;那么,对我来说,它在以下两个主要领域比其他机器有所进步。
首先,它接近本质——例如,如果您将图像处理库编译到WebAssembly并运行它,与将其编译到x86-64或ARMv8或其他版本相比,您将获得类似的性能。(特别是对于图像处理,本地运行通常仍然会获胜,因为WebAssembly中的SIMD(单指令多数据流)原语更窄,而且将图像放入和取出WebAssembly可能意味着一个要创建一个副本。)WebAssembly的指令集涵盖了广泛的低级别操作,这使编译器能够生成高效的代码。
这里的新颖之处在于WebAssembly既可移植,又很成功。我们这些程序员“怪人”知道,仅仅在技术上做得更好是不够的:你还必须成功地为你的替代方案争取吸引力。
第二个有趣的特征是,WebAssembly(一般来说)遵循最小权限体系架构:WebAssembly模块从一开始就只能够访问它自己。模块实例所具有的任何功能都必须在实例化时由主机显式地与其共享。这不同于可以访问所有主内存的DLL,也不同于可以改变全局对象的JavaScript库。这一特性使WebAssembly模块能够可靠地组成更大的系统。
11、大肆宣传WebAssembly吧
所有浏览器都支持WebAssembly!因此,您的代码能够提供给世界上的任何人使用!
它就运行在你的身边!从靠近用户的网站运行代码!
把一个库(例如Expat)组合到你的浏览器程序(例如Firefox)中,不需要冒任何风险!
这是一种新的轻量级虚拟化:Wasm正相当于容器对虚拟机的作用!因此,不再需要耗费花在Kubernetes上的现金!
同样,WebAssembly正在取得成功!它在你所有的手机、所有的桌面web浏览器、所有的内容分发网络上;在某些情况下,它似乎会取代云中的容器。
原文链接:https://www.wingolog.org/archives/2023/03/20/a-world-to-win-webassembly-for-the-rest-of-us
译者介绍:
朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。