WebAssembly 动态 | WebAssembly 的发展风险及Wasmtime 1.0 性能概览

2022-12-08 21:03:06 浏览数 (1)

本篇文章关注 WebAssembly 的相关动态。

Fermyon | WebAssembly 的风险

今早看到来自 fermyon 官方博客的文章[1],介绍了 WebAssembly 现存的一些风险和他们的应对方法:

  • 标准化进展非常缓慢。为此 Fermyon 加入了 字节码联盟 亲自推动标准化进程,并通过构建 Spin 代码实现来充分利用标准
  • 语言支持不够。Fermyon 认为前 20 种语言中至少有 15 种必须完全支持 WebAssembly 以及 WASI 和组件,才能正确地认为 WebAssembly 被很好地采用。Fermyon 采取的立场是将注意力集中在最受欢迎的语言上,这就是为什么使用 Rust 而不是 C 或Zig。

“这方面也有一些好消息:

  1. 语言支持正在迅速增长,今年C#、Python和Ruby都增加了支持
  2. wasi 支持现在是进入 wasm 游戏领域的筹码
  3. 在主流实现语言未能发挥作用的地方,该语言的替代实现正在加紧发展,比如 tinygo 对于 wasm 的支持就超越了 go
  • 生态系统不是可选的。WebAssembly 有望成为下一波计算浪潮,但除非 Fermyon 能围绕它建立生态系统,所以 Fermyon 正在努力联合相关其他企业合作共建社区。
  • 社区实现碎片化。(比如 deno 和其他非标准化实现,文章没有明说)。可悲的是,有时这仅仅是由于无知和不参与:“我们不知道有一个标准为此而出现”。所以目前发布的组件规范(正在进行中,但正在迅速成熟)旨在解决这类问题,这个标准使得在不同的主机实现之间共享 WebAssembly 二进制文件成为可能。还有一个不幸的趋势,即一些开发人员选择与组件模型相反的工作,创建与他们自己的主机运行时的强链接。走这条路一方面会导致平台锁定,另一方面会毫无意义地重新编写相同的代码(针对略有不同的主机进行工具化)。幸运的是,那些准备最好的人(Fastly、Mozilla、Microsoft)反而选择推动互操作性标准以造福所有人。这是正确的第一步。为了阻止破坏性的碎片化“手榴弹”,我们必须增加社会压力,不要我行我素,而要坚持互操作性标准。做到这一点的一个关键方法是彼此公开合作(通过字节码联盟、W3 和 CNCF 等组织),不仅要创建和实施标准,还要创建对话论坛。

“Fermyon 的愿景是,在五年内,WebAssembly 将成为常态,而不是小众市场。新一波应用程序将能够利用 WebAssembly 的速度、安全性和组件模型。为了实现这一目标,我们每个人都可以发挥作用。

Wasmtime 1.0 性能概览

近日字节码联盟发布了 wasmtime 1.0 性能概览[2] 的一篇文章,为将在 9.20号发布的 wasmtime 1.0 稳定版做前期铺垫,介绍了 wasmtime 团队近期在编译器和运行时中所做的工作。这里只做重点摘要,并非全文翻译,对细节感兴趣的可进一步参阅原文。

什么是性能

让 Wasmtime 和 Cranelift 变得更快意味着什么?所谓的“快”是什么意思?

“Cranelift 也被用于 Rust Debug 模式编译后端

当 Wasmtime 执行 Wasm 程序时,CPU既执行从Wasm字节码编译的本地指令,也执行 "Wasmtime Runtime "的一部分,Wasmtime Runtime 用于维护数据结构以帮助实现Wasm语义。这两部分的执行有两个阶段:启动初始化(Wasm代码的编译,和运行时的初始化)和 稳态(steady-state)执行。这两个层面的四个组合都对性能有一定的影响,可以分别进行优化。

Compiler (Cranelift)

Runtime (Wasmtime)

启动阶段

代码编译时间

Wasm 模块实例化时间

稳态阶段

生成代码的速度

运行时的基本速度

wasmtime 对于改善这四个象限中的每一项都做了大量工作。

Wasm 模块实例化

WebAssembly 之所以安全是因为wasm 模块每个实例与生俱来的隔离性。为了有效地利用这种隔离性,Wasm的一些应用将每一个工作单元实例化为一个新的实例,例如服务器上每个传入的请求。因此,极快的模块实例化是像Wasmtime这样的Wasm VM的一个关键要求。

现在 wasmtime 的模块实例化速度已经被优化到了微秒级别。这是如何做到的呢?

虚拟内存技术

在过去,wasmtime 是通过为 wasm 应用初始化一大块内存(通过malloc或mmap或一些其他分配器),然后将数据复制到正确的位置。

现在,是从现代计算机使用的虚拟内存技术获得灵感,实现了一个 实例分配器[3]使用了mmap 、madvise 和写时复制(copy-on-write)的技术将实例化的成本大大的降低了。

延迟初始化

Wasmtime运行时在开始执行已编译的Wasm代码之前,要花费大量时间来初始化数据结构。所以,团队为函数引用表和它们所指向的函数闭包对象实现了延迟初始化[4]

优化结果

SpiderMonkey.wasm 的实例化时间从大约2毫秒到5微秒,快了400倍。

运行时性能

Wasm 执行过程中的大部分 CPU 时间通常花在Wasm程序本身,或它调用的 "hostcalls"(这是Wasmtime用户插入Wasmtime的代码,无法直接控制),除此之外,Wasmtime本身有一些部分在某些情况下必须运行,这部分代码就是 Wasmtime Runtime 的性能优化之处。

加速栈走查(Stack-Walking)

之前,为了让Wasmtime列举所有的栈帧(stackframes),Cranelift编译器产生了所谓的 "unwind info"。这是一种元数据,描述了编译后的代码将在任意给定点上把值放在栈中。利用这些元数据,Wasmtime的 "unwinder"能够逆向程序状态:它理解一个活动函数每次调用的栈帧,最终找出谁调用了它,并在栈上迭代,直到它到达Wasm的初始入口。整个过程非常慢。

团队对此进行了改进,确保始终保持一个帧指针的链表,从而达到栈走查像遍历链表那么简单。这种性能改进是一个巨大的质量改进:它允许启用栈跟踪,并大幅提高Wasmtime的健壮性。

加速多任务协作 与 代际(Epoch) 中断

Wasmtime的一个常见用例是同时并发运行许多不同的 WebAssembly guests,并在它们之间设置时间片。Wasmtime内置支持在一个异步事件循环上运行对Wasm的调用。

Wasmtime 用户在这种情况下可能遇到的一个问题是如何限制 Wasm 程序的执行时间。通常,当与事件循环异步运行时,计算密集型任务应拆分为多个段,以便事件循环不会停止超过最大“时间片”。

通过将 Wasm 字节码标准编译为本地机器代码,Wasm 中的循环成为编译代码中的循环,并运行尽可能多的迭代,没有限制。如果用户从事件循环中调用此函数,则该事件循环可能会无限期停止。

因此,特别是在运行不受信任的代码时,Wasmtime 用户必须建立一种在一定时间限制后重新获得控制权的方法。所以 Wasmtime 必须提供一种在某个时间点中断 Wasm 执行的方法。

之前,实现这一行为的主要方式是通过“燃料(fuel)”。这是一种机制,通过该机制,已编译的 Wasm 代码增加了对“操作”进行计数的代码,根据限制检查当前计数,如果超出限制,则返回给调用者或事件循环。“燃料(fuel)”是一种有效的机制,但它成本很高:它需要用“计数”来扩充每一段代码,并经常将该计数存储到内存中并检查它。

团队使用了基于代际的中断[5]取代了 “燃料(fuel)”机制,性能提升了两倍。

“使用 “燃料”机制还是代际中断,是一种权衡。“燃料”机制更加精准,而代际中断性能更好。

Cranelift 编译代码的质量

Cranelift 用于将Wasm字节码编译成计算机可以直接执行的本地机器代码。

寄存器分配器改造:regalloc2

在过去的一年里,wasmtime引入了新的寄存器分配器 regalloc2[6]。寄存器分配器是编译器的一个部分,它为程序中的值分配存储位置。在真正的CPU中,指令对寄存器中的数据进行操作,寄存器是一些小的存储位置,每个位置可以容纳一个值(例如,一个64位的数字)。寄存器分配器决定在什么时候将哪些值保存在哪些寄存器中。做好这一点可以大大改善程序的性能,因为它意味着更少的数值移动。WebAssembly,作为一个抽象的、与硬件无关的虚拟机,没有大多数指令的输入和输出位置的概念。

regalloc2的设计是为了支持更高级的算法,以决定如何向寄存器分配数值。当引入时,它将SpiderMonkey.wasm的运行时性能提高了约5%,将另一个CPU密集型基准测试bz2的性能提高了4%

更好的模式管理:新的后端、ISLE和持续的调整

指令选择问题是指选择最佳的CPU指令来实现一个给定的程序行为。因为每个CPU都有自己独特的指令集,而且这些指令可以以许多不同的方式组合,这是一个非常难解决的组合难题。Cranelift最初采用了一种新的编译器后端设计,可以实现更高级的模式匹配,目前采取的方法是用模式匹配DSL(特定领域语言)来表达底层的指令,这样我们就可以更容易地调整这些模式。

未来:中端优化

在未来,我们计划为Cranelift引入更先进的中端优化。中端优化器 "是编译器的一部分,在程序被 "降级"为机器特定的形式之前(也就是在指令选择之前),以各种方式对程序进行转换,使其更快。有一套经典的优化方法,几乎所有的编译器都会执行,包括简化常数表达式(1 1变成2)等基本规则。但也有许多更复杂和微妙的转换。

Cranelift: 编译时优化

除了优化Cranelift生成的代码,编译过程本身如果太慢,那么Wasmtime可能需要很长的时间来启动新的代码,将会阻碍生产力(对于Wasm开发人员)和响应能力(对于访问新应用程序的最终用户)。因此,编译器的速度是一个重要的指标。

广义上讲,我们可以通过在后端关键部分选择更好的算法来提高编译时间,如寄存器分配器或优化通道,或通过做一般的程序优化,如减少内存使用。

regalloc2

切换到 regalloc2 显着改善了编译时间,因为寄存器分配占编译时间的很大一部分:测量单线程时间(不是并行编译),SpiderMonkey.wasm 的构建速度提高了 6%,bz2 的构建速度提高了 10%。

中端优化器:将多个passes合并为一

算法重新设计也可以大大缩短编译时间。在我们的中端优化器原型中,我们对编译器设计的相关部分采取了一种新的方法:几个不同的 "程序",或以某种方式改造程序的特定算法,被合并成一个统一的框架,只对程序进行一次处理。

标准程序优化

一种特别有效的提高速度的改变是减少内存的分配和使用。程序分配的内存越少,它的运行速度就越快,至少有两个原因:内存分配器本身可能很慢,而且使用更多的内存也会导致更多的缓冲区未命中和内存流量。由于其多线程编译模式,Cranelift也倾向于对分配器施加特别大的压力。

总结

高性能是任何希望成为构建高效、持久系统的基础的软件的一个关键方面。如果 WebAssembly 想要成功,它的运行速度必须能达到与本地代码竞争的水平。这也是 wasmtime 性能优化的终极目标。

通过该篇文章我们简单了解了 Wasmtime 和 Cranelift 性能优化的相关工作,以及当前 wasmtime 1.0 的性能状态(详细数据见原文)。后续的文章将介绍该团队如何确保 Wasmtime 安全以及编译器生成正确的代码。

参考资料

[1]

文章: https://www.fermyon.com/blog/risks-of-webassembly

[2]

wasmtime 1.0 性能概览: https://bytecodealliance.org/articles/wasmtime-10-performance

[3]

实例分配器: https://github.com/bytecodealliance/wasmtime/pull/3697

[4]

实现了延迟初始化: https://github.com/bytecodealliance/wasmtime/pull/3733

[5]

基于代际的中断: https://github.com/bytecodealliance/wasmtime/pull/3699

[6]

regalloc2: https://github.com/bytecodealliance/regalloc2

0 人点赞