6 种 WebAssembly 的优化手段
简称为 Wasm 的 WebAssembly 是一种二进制格式,包括 Rust、C、JavaScript、Python、Ruby、.NET 在内等诸多语言都可通过 Wasm 执行。此外,Wasm 也可运行在各种硬件和操作系统之上,其规范的设计快速且紧凑,以及重中之重的安全。
在 2022 年,最初仅为浏览器设计的 Wasm 已经在其他领域大放异彩,实践证明,Wasm 在嵌入式编程、插件、云、边缘计算等领域都非常有用。在这些用例中,性能都是极其重要的因素。快速加载可执行部分是性能中的一环,其中文件的大小往往对原始性能有直接的影响。
在本文中,我们将探讨六种优化 Wasm 性能及文件大小的方法。
编程语言之间或多或少都些许区别,其中之一是语言执行时对运行时大小的需求。底层系统语言如 C 或 Rust 都算是轻量级,只需要很小的运行时开销。其他如 Swift 等语言对运行时的需求不小。Swift 的二进制中包含了很多内置行为,因此文件也大多不会小。同理,Java 和 .NET 语言的二进制文件也往往很大。
为展示这其中区别,让我们看看一段“Hello World”程序在 Rust 和 Swift 中的表现。
Rust 中一段简单的“Hello World”如下:
fn main() { println!("Hello, world!");}
用 cargo build —target wasm32-wasi
命令编译后的二进制文件大小为 2.0 M。这是未经优化的文件大小,后文中我们会再回到这点上。
同样的程序在 Swift 中如下:
print("Hello, World!\n")
通过 Swiftwasm 中的 swiftc -target wasm32-unknown-wasi hello.swift -o hello.wasm
命令编译至 Wasm 后,会产生 9.1M 的镜像。可见 Swift 版本的程序相较 Rust 而言大了四倍有余。
因此,编程语言的选择会直接影响二进制文件的大小,并在一定程度上影响启动的时间。但对文件大小的优化并不是到此为止了,我们还有其他手段可以进一步优化二进制的大小。
部分编译器提供了内置的编译选项,以优化其所生成的二进制。C/C++ 的老手们对此并不陌生,而新生语言如 Rust 及 Zig 也提供优化选项。
在上文中简单的三行 Rust 程序中,我们通过默认编译命令cargocommand
得到了 2.0M 的二进制文件。但在加上编译选项之后,我们还可以进一步缩小文件大小。cargo build --target wasm32-wasi --release
命令会输出 1.9M 的二进制文件。因为 Rust 的 svelte 运行时存在,这种小型程序能优化掉东西并不多。但对于大型项目而言,--release
选项可以显著减少文件大小。以 Bartholomew CMS 项目为例,默认编译命令会生成 84MB 的二进制文件,而启用 --release
选项的编译则会将文件大小缩减至 7M,效果不可谓不明显。
Rust 中的 --release
选项能做的可不仅仅是缩小文件大小,它还能移除调试器和分析工具所用的符号,从而加快执行速度。在生产环境中的代码执行方面,这可是个非常有用的功能。运行完整 84M 的 Bartholomew 需要数秒的执行时间,但优化后的 7M 文件执行仅需要几毫秒。
并不是所有编译器都提供优化的选项,即使是提供优化选项的编译器可能也不会有十分明显的优化效果。
Wasm 的优化工具可以分析 Wasm 二进制文件稳健性的同时,进一步优化文件大小,甚至还可优化 Wasm 可执行文件的性能特征。Binaryen 项目中提供了诸多 Wasm 可用的命令行工具,其中就包括 wasm-opt 优化器。
有 9.1M 大小的 Swift 程序珠玉在前,让我们看看用 wasm-opt 工具运行 -O hello.wasm -o hello-optimized.wasm
能带给我们什么。这条命令生成了一份优化后的二进制文件 hello-optimized.wasm
,大小仅有 4.0M,缩小了 50% 有余。
wasm-opt 工具提供了多项对二进制的优化,从重复代码移除到代码整理不等。但这里说的“代码”是指 Wasm 指令,而非开发者编写的源码。因此,运行 wasm-opt 工具并不会修改 Swift 源码,仅仅是重写了 Wasm 二进制。这种方式不仅削减了文件大小,同时也优化了运行时性能。在作者的电脑上,优化后的“Hello World”程序执行速度比没经过优化的要快上两倍。
不仅如此,wasm-opt 工具甚至还能进一步优化已经经过优化的 Rust 代码。让前文中 1.9M 的 Rust 二进制进一步压缩至 1.6M。但在这种简单程序上的优化并没有给性能带来多少提升,无论是否进一步优化,运行时间均在十分之一秒左右。但或许更为大型的 Rust 二进制文件可以通过 wasm-opt
获得运行速度的改善。
二进制格式 Wasm 非常灵活,可以通过 wasm3 这类解释器(如 )按序读取并分块执行,而另外一些 Wasm 运行时,如 Wasmtime,则是借助了 JIT(即时)编译技术,加快了执行的速度。
解释器的优势在”Hello World“这种简单程序、或运行于设备资源有限(如 Raspberry PI)的程序,因为它可以用更少的资源做更少的事。但对于 Bartholomew CMS 这类大型程序而言,JIT 形式的运行时拥有更多优势。这是因为 JIT 编译器会在启动以及执行早期进行额外工作,以优化程序的存内显示,而这种优化也会继续存在于程序的持续运行中。但因为 JIT 过程需要时间,所以对于只运行一小段时间的小型程序而言,反倒是一种性能的损失。
那么我们要如何选择呢?按照传统经验论的说法,如果是在比 Raspberry PI 资源还要有限的设备上运行,那么就用解释器,不然就还是选择支持 JIT 的运行时吧。
说起运行时,还有另一个技巧。
JIT 运行时会在启动时进行存内优化。但如果我们想在一次优化执行后,将其写回磁盘并在程序的下次运行时重复利用优化呢?这就是“提前(AOT)”编译了。
但 AOT 编译阶段所做的优化内容与之 wasm-opt 的优化有本质上的不同,这也是 AOT 编译的一大缺点。AOT 的优化因为考虑到了操作系统和处理器结构,所以优化后的 Wasm 二进制文件无法移植再移植到其他机器上。除此之外,优化后文件的格式也因运行时的不同而各异,也就是说,一个 AOT 编译的 Wasm 运行时程序无法再被其他 Wasm 运行时执行。
Wasmtime 运行时可将 wasm 模块编译为 AOT 格式,用 wasmtime compile hello.wasm
命令编译之前的 Swift 例子,会生成一个可被 Wasmtime 执行的新文件hello.cwasm
。当然,对于“Hello World”这种小程序而言,AOT 编译效果并不明显。但在处理大型程序时,AOT 编译的性能会比解释器或 JIT 运行都要高。不过需要注意的是,多数 AOT 编译器所生成的二进制文件比其等效 Wasm 文件都要大,这是因为 Wasm 运行时中的很多自身元素都会被编译至二进制文件以提高性能。
什么时候该用 AOT 编译器呢?一个很具体的经验论是,只有在确定程序只会在同一套 Wasm 运行时、操作系统、架构配置下运行时再选择 AOT。此外,Wasm 模块应以正常的 Wasm 形式分发,并只在安装中或安装结束后再进行 AOT 编译。
第五种优化手段可以说是最神奇的一种。因为 Wasm 是基于堆栈的虚拟机,不仅可以随时停止,还能被写入到磁盘供后续恢复,当然这其中也有限制,但这些对本文的主题并不重要。Wasm 的这个功能有个蛮有趣的应用。
代码中时常会有一部分需要在每次启动时都运行,这部分代码做的事可能也很平常,像是设置变量默认值、创建数据结构实例等等。但每次运行程序时都必须执行同一套初始化逻辑,而每次运行的结果状态也不会有什么区别:变量被初始化为同样的值,数据结构被初始化为同样的状态。
如果我们能在第一次运行初始化的时候,将 Wasm 状态快照后写回磁盘,那么在后续程序执行的时候,我们就拥有了已经完成的状态,是不是就不用再运行初始化步骤了呢?
这个想法组成了 Wizer 项目,Wizer 提供对初始化代码块添加注释,让其在一次执行后被写入一个新的初始化后 Wasm 二进制文件。与 AOT 编译不同,这个新的二进制文件与别的 Wasm 二进制没什么区别,因此依旧是可移植的。
在用的时候可能会感觉 Wizer 有点不太稳定,但 .NET 这类系统可以从 Wizer 中受益良多。
根据我们在 Fermyon 的经验来看,优化对开发者工具和云运行时都很重要,但这二者的情况并不相同。
对开发者而言,编译器所能提供的优化工具中用得越多越好。比如在编译 Rust 代码时,我们总会带上 --release
选项。我们的开源工具 Spin,允许开发者用多种语言构建 WebAssembly 微服务及网页应用,其中不乏有各种语言模板自己的优化内容。此外,在本地编译中包含 wasm-opt 也很有用,尤其是对于需要大量运行时的语言。开发过程中我们选择的运行时是支持 JIT 的,因为开发阶段 AOT 编译的价值不大。
服务器端就是另一个故事了。以我们基于 SaaS 的 Wasm 运行时平台为例,Fermyon 云 仅接受 Wasm 二进制输入,但在部署到云集群后,这些二进制又变成了通过 AOT 编译过后的文件。因为我们非常清楚主机运行时的配置,所以这种方式很可靠。这些 Wasm 文件被部署到 Arm64 系统后可以相应地被 AOT 编译,我们不用担心这些文件在英特尔的架构上的执行情况。
至于 Wizer,我们其实只在 .NET 上用过,Wizer 在这方面的优化非常好用。
这 6 种优化 Wasm 性能及文件大小各有自己的优缺点,结合使用其中一些方法也可以增加效益。在生产的 Wasm 环境中应用这些手段也会有益处。
原文链接:
The Six Ways of Optimizing WebAssembly(https://www.infoq.com/articles/six-ways-optimize-webassembly/)
相关阅读:
WebAssembly 在工业领域的巨大机遇 (https://www.infoq.cn/article/pskeeKXTSbmQa2cauwBh)
Docker 发布 WebAssembly 支持工具预览版 (https://www.infoq.cn/article/hipvRmYj1awjevsGI8Ps)
声明:本文为 InfoQ 翻译,未经许可禁止转载。
点击底部阅读原文访问 InfoQ 官网,获取更多精彩内容!
微信扫码关注该文公众号作者