本篇文章关注 WebAssembly 的相关动态。
今早看到来自 fermyon 官方博客的文章[1],介绍了 WebAssembly 现存的一些风险和他们的应对方法:
“这方面也有一些好消息:
“Fermyon 的愿景是,在五年内,WebAssembly 将成为常态,而不是小众市场。新一波应用程序将能够利用 WebAssembly 的速度、安全性和组件模型。为了实现这一目标,我们每个人都可以发挥作用。
近日字节码联盟发布了 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 对于改善这四个象限中的每一项都做了大量工作。
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 的性能优化之处。
之前,为了让Wasmtime列举所有的栈帧(stackframes),Cranelift编译器产生了所谓的 "unwind info"。这是一种元数据,描述了编译后的代码将在任意给定点上把值放在栈中。利用这些元数据,Wasmtime的 "unwinder"能够逆向程序状态:它理解一个活动函数每次调用的栈帧,最终找出谁调用了它,并在栈上迭代,直到它到达Wasm的初始入口。整个过程非常慢。
团队对此进行了改进,确保始终保持一个帧指针的链表,从而达到栈走查像遍历链表那么简单。这种性能改进是一个巨大的质量改进:它允许启用栈跟踪,并大幅提高Wasmtime的健壮性。
Wasmtime的一个常见用例是同时并发运行许多不同的 WebAssembly guests,并在它们之间设置时间片。Wasmtime内置支持在一个异步事件循环上运行对Wasm的调用。
Wasmtime 用户在这种情况下可能遇到的一个问题是如何限制 Wasm 程序的执行时间。通常,当与事件循环异步运行时,计算密集型任务应拆分为多个段,以便事件循环不会停止超过最大“时间片”。
通过将 Wasm 字节码标准编译为本地机器代码,Wasm 中的循环成为编译代码中的循环,并运行尽可能多的迭代,没有限制。如果用户从事件循环中调用此函数,则该事件循环可能会无限期停止。
因此,特别是在运行不受信任的代码时,Wasmtime 用户必须建立一种在一定时间限制后重新获得控制权的方法。所以 Wasmtime 必须提供一种在某个时间点中断 Wasm 执行的方法。
之前,实现这一行为的主要方式是通过“燃料(fuel)”。这是一种机制,通过该机制,已编译的 Wasm 代码增加了对“操作”进行计数的代码,根据限制检查当前计数,如果超出限制,则返回给调用者或事件循环。“燃料(fuel)”是一种有效的机制,但它成本很高:它需要用“计数”来扩充每一段代码,并经常将该计数存储到内存中并检查它。
团队使用了基于代际的中断[5]取代了 “燃料(fuel)”机制,性能提升了两倍。
“使用 “燃料”机制还是代际中断,是一种权衡。“燃料”机制更加精准,而代际中断性能更好。
Cranelift 用于将Wasm字节码编译成计算机可以直接执行的本地机器代码。
在过去的一年里,wasmtime引入了新的寄存器分配器 regalloc2[6]。寄存器分配器是编译器的一个部分,它为程序中的值分配存储位置。在真正的CPU中,指令对寄存器中的数据进行操作,寄存器是一些小的存储位置,每个位置可以容纳一个值(例如,一个64位的数字)。寄存器分配器决定在什么时候将哪些值保存在哪些寄存器中。做好这一点可以大大改善程序的性能,因为它意味着更少的数值移动。WebAssembly,作为一个抽象的、与硬件无关的虚拟机,没有大多数指令的输入和输出位置的概念。
regalloc2的设计是为了支持更高级的算法,以决定如何向寄存器分配数值。当引入时,它将SpiderMonkey.wasm
的运行时性能提高了约5%
,将另一个CPU密集型基准测试bz2
的性能提高了4%
。
指令选择问题是指选择最佳的CPU指令来实现一个给定的程序行为。因为每个CPU都有自己独特的指令集,而且这些指令可以以许多不同的方式组合,这是一个非常难解决的组合难题。Cranelift最初采用了一种新的编译器后端设计,可以实现更高级的模式匹配,目前采取的方法是用模式匹配DSL(特定领域语言)来表达底层的指令,这样我们就可以更容易地调整这些模式。
在未来,我们计划为Cranelift引入更先进的中端优化。中端优化器 "是编译器的一部分,在程序被 "降级"为机器特定的形式之前(也就是在指令选择之前),以各种方式对程序进行转换,使其更快。有一套经典的优化方法,几乎所有的编译器都会执行,包括简化常数表达式(1+1变成2)等基本规则。但也有许多更复杂和微妙的转换。
除了优化Cranelift生成的代码,编译过程本身如果太慢,那么Wasmtime可能需要很长的时间来启动新的代码,将会阻碍生产力(对于Wasm开发人员)和响应能力(对于访问新应用程序的最终用户)。因此,编译器的速度是一个重要的指标。
广义上讲,我们可以通过在后端关键部分选择更好的算法来提高编译时间,如寄存器分配器或优化通道,或通过做一般的程序优化,如减少内存使用。
切换到 regalloc2 显着改善了编译时间,因为寄存器分配占编译时间的很大一部分:测量单线程时间(不是并行编译),SpiderMonkey.wasm 的构建速度提高了 6%,bz2 的构建速度提高了 10%。
算法重新设计也可以大大缩短编译时间。在我们的中端优化器原型中,我们对编译器设计的相关部分采取了一种新的方法:几个不同的 "程序",或以某种方式改造程序的特定算法,被合并成一个统一的框架,只对程序进行一次处理。
一种特别有效的提高速度的改变是减少内存的分配和使用。程序分配的内存越少,它的运行速度就越快,至少有两个原因:内存分配器本身可能很慢,而且使用更多的内存也会导致更多的缓冲区未命中和内存流量。由于其多线程编译模式,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