WebAssembly 常见引擎简介

413次阅读  |  发布于7月以前

1. 前言

在本文中,我们将讨论驱动 WebAssembly 程序运行的核心组件——引擎。首先,本文将简要介绍一个语言的引擎包括哪些主要组成部分,它们如何配合完成工作,尝试构建一个概念模型。之后,就几款社区流行的开源引擎,分别介绍各自的特点。

2. 引擎通用架构

在这里,我们要谈论的引擎总是与某种语言绑定,也可称之为虚拟机。那么一个引擎如何驱动一段程序运行,并得到结果呢?

第一,我们需要将程序文件加载到内存中,并解析符号,将其中的符号转化成具体的、可访问的内存地址。这一步需要一个加载器和一个链接器;

第二,我们需要执行目标程序,完成其中的每一条指令。如果是 JavaScript 这类脚本程序,我们还需要一个编译组件,将程序文本编译为指令序列。面对指令序列,引擎可以解释执行,也可以进一步编译为物理机器的可执行指令后执行。这一步需要一个能够解释或者编译执行的执行器;

第三,目标程序可能是单线程执行,也可能是并发执行,为了支持并发,要求存在一种调度机制来管理线程。这一步需要一个线程调度器;

第四,一般的程序总是会要求动态内存的分配,用于存储动态数据,那么就要有组件负责分配内存。考虑到内存资源的有限性,不能无限制增长,那么就要有组件负责回收内存。这一步需要一个内存管理器;

最后,目标程序可能要求获得各种外部资源,如访问文件系统、监听网络端口等,引擎必须提供访问这些资源的通道。这一步则需要语言扩展或者外部访问接口。

图 1. 引擎的概念模型

有了这几个组件,一个引擎就可以顺利驱动程序运行了。接下来,结合 WebAssembly 的特点,我们一起看一看市面上流行的几款 wasm 引擎。

3. 常见的 WebAssembly 引擎

在这一部分中,我们将着重介绍几个活跃在 WebAssembly 社区的引擎。这些项目都有着比较广泛的使用群体,在不同的场景下为用户创造了价值。接下来,我们将从项目背景、显著特征、性能表现等方面介绍以下几款引擎。其中,我们会更加深入地讨论 wasmtime 和 wasm3 的结构和设计。

3.1 wasmtime

wasmtime 是非盈利组织——字节码联盟(Bytecode Alliance)旗下的 WebAssembly 引擎,使用 Rust 语言编写开发,是一种高性能编译型 wasm 引擎。

3.1.1 结构特点

如图 2 所示,wasmtime 总体上可以分为编译、运行时和工具三部分。

图 2. wasmtime 的总体结构

编译

这一部分包含 4 个组件,从上到下分别是wasm-environwasm-craneliftwasm-jitwasm-obj,这个顺序在一定程度上反映了 wasmtime 完成编译的时序。

从内部视角来看,wasm-environ是编译的入口,但实际上wasm-cranelift会负责执行函数级别的即时编译,为每一个函数生成 JIT 代码。得到 JIT 代码之后,需要存储在可执行内存区域才能被实际执行,这一步依赖wasm-jit对引擎内部可执行内存的管理。另外,wasm-obj会根据编译过程中产生的各类信息,生成 ELF(Executable and Linkage Format)映像,其中包括所有的编译得到的函数以及跳板(Trampoline)函数、用于链接阶段的重定位记录等内容。如果 wasm 模块中存在 DWARF 信息,则会被写入对应模块 ELF 映像的.dwarf段。

可见,在 wasmtime 的世界中,一个 wasm 模块被加载到内存并编译之后,其内存映像跟一个普通的 ELF 文件非常相似。其中,所有的 wasm 函数都会被当作原生函数来完成链接和调用。这也是为什么 wasmtime 可以借助 lldb 调试 wasm 程序(关于 WebAssembly 调试,可以参考第十章)。

运行时

这一部分的主体就是wasm-runtime,它将维护 wasm 模块在运行时的各类数据结构,为程序运行提供必要的支持。wasm-runtime组件负责维护两类重要的实体:StoreInstanceHandle,见表 1。

名称 作用
Store
  • 作为实际存储 wasm 模块各类数据结构的容器,包括 function、memory、table、element等;
  • 作为隔离的基本单元,不同 store 中的数据结构不允许互操作,类似于 V8 中的 Isolate.
InstanceHandle
  • 作为 wasm 模块实例的底层表示,但也用于宿主定义的对象;
  • 归属于某个 Store,同时也包含一个 VMContext 用于维护一个 wasm 模块实例所有的数据结构.

表 1. Store 和 InstanceHandle

从图 3 中可以大致了解这几类实体之间的关联。其中,Engine可认为是 wasmtime 作为运行时的实例,可被视作顶级上下文或根上下文,通常情况下单个进程内部只会创建一个 Engine,而每一个Engine内部可以创建多个Store

图 3. wasm-runtime 的关键数据结构

wasmtime-runtime之外,运行时部分还存在组件wasmtime-fiber用于支持异步功能,主要处理栈的切换。

工具

工具部分包含以下三个子模块:

由上文可知,wasmtime 引入了 Cranelift 作为编译器后端,在将 wasm 代码转译成 Cranelift 中间表示(IR)之后,提前或者在运行时生成对应平台的可执行指令序列。因此,相对于使用解释器模式的引擎,其运行速度要更快,wasmtime 在 CoreMark Benchmark 上的实验性能达到了 wasm3 的 4 倍。

3.1.2 项目现状

作为 Bytecode Alliance 推出的拳头项目之一,wasmtime 对于 WebAssembly 相关标准支持的完整度非常高。它支持了标准的 WASI,实现了标准的 wasm c-api,并紧密跟踪 WebAssembly 核心特性。截止本文写作时间,wasmtime 不仅实现了 Fixed-Width SIMD、Reference Types、Bulk Memory operations 等成熟提案,还支持 Tail-Call、Threads 和 Garbage Collection 等还处于标准实现阶段的提案。

虽然 wasmtime 使用 Rust 语言开发完成,但是为了在不同的宿主语言中使用,wasmtime 团队提供了 C/C++、Python、.NET、Go 以及 Ruby 等语言接口,便于不同偏好与背景的开发者引入 wasmtime.

2022 年 9 月份,wasmtime 1.0 版本正式发布。截止当时,接入 wasmtime 的组织包括:Shopify、Fastly、DFINITY、InfinyOn Cloud 以及 MicroSoft,主要应用于 Serverless区块链等场景。根据 wasmtime 团队报告,这些组织引入 WebAssembly 并切换到 wasmtime 之后,均在自身的业务中获得良好的收益。如电商公司 Shopify 的相关业务就因此获得平均 50%的执行性能提升。

3.2 wasm3

wasm3 是一款基于解释器执行的轻量级 WebAssembly 引擎,使用 C 语言编写开发,拥有比较完善的 wasm 运行时系统,主要贡献者包括乌克兰开发者 Volodymyr Shymanskyy 等人。

3.2.1 结构特点

wasm3 最大特点就是依靠纯解释器执行所有的 WebAssembly 指令,没有引入 JIT/AOT 编译。根据 Benchmark 数据,自 wasm3 出现在社区并逐渐成熟之后,在相当一段长的时间内,wasm3 都是运行速度最快的解释型引擎。

这其中,一个关键的设计就是指令线索化(threaded code) 。与平凡的 switch-case 模式不同,线索化的解释器并不存在一个外层的控制结构。相反地,线索化指令总是会在自身解释程序的最后位置进行对下一条指令的调用,从而“自驱动”地执行所有指令。也正是由于指令解释函数总是以对下一条指令的调用结束,编译器可以对大部分的指令操作进行尾调用(tail-call)优化,以减少函数调用栈帧的压入和弹出操作。

另外一个关键设计是寄存器指令转译。在此前的课程介绍中,我们了解到 WebAssembly 基于栈式机器进行设计。而根据已有的研究结论[1],基于寄存器的指令运行速度会更快,因此 wasm3 也将 wasm 指令序列转译成了更直接和高效的寄存器指令序列(简称 M3 指令)。M3 指令的解释函数,都有一个固定的、共同的函数签名:

// 其中,mem表示函数所属 wasm 模块实例的线性内存,r0用于存放整型参数,fp0则用于存放浮点型参数,
// 其余的pc、sp都是自解释的命名,不赘述。
void * Operation_Whatever (pc_t pc, u64 * sp, u8 * mem, reg_t r0, f64 fp0);

wasm3 对一个 wasm 函数进行转译之后,生成的 M3 指令序列由指令的解释函数地址、立即数、元数据(如函数所属模块实例指针)等组成。

举个例子,如图 4 所示,模块 x 中存在一个名为 func 的函数,其中有table.size的指令,这条转译之后将变为函数op_tableSize的地址加上表示模块指针的立即数组成的 M3 指令。由于 wasm3 目前还只支持单个 table,所以在执行时直接取出模块实例唯一的 table 的容量值并存入r0寄存器中,然后调用下一条指令的解释函数。这样,我们也就大致了解了 wasm3 内部指令的执行模式。

图 4. M3 指令形式

3.2.2 项目现状

由于是纯解释执行, wasm3 可以在 iOS 等设备上运行。而这一点是其它纯编译型引擎无法做到的,这也赋予了它独特的跨平台优势。再考虑到它的轻量特点——移动端二进制产物体积不到 70 KB,作为移动端引擎,wasm3 可谓表现优异。

在宿主语言支持方面,wasm3 社区也有诸多亮点:开发者不仅能够使用 C/C++ 低成本接入 wasm3 引擎,而且可以在 Python、Rust、GoLang、Zig、Perl 等编程语言中将 wasm3 作为库轻松引入。

另外,wasm3 通过支持标准的 WebAssembly System Interface(WASI)提供获得系统能力的通道,因此能够以独立(Standalone)的方式运行 wasm 函数,即使它依赖于如printf之类的接口。比如,通过 CLI 工具执行 wasm 函数:

brew install wasm3
wasm3 --func function_name_to_run your_module.wasm

美中不足的是,目前 wasm3 对于社区标准规范的支持还有待完善,包括已被纳入核心规范的多线性内存、引用类型、异常处理等提案。尽管如此,wasm3 的诸多特点,还是吸引了众多对 WebAssembly 感兴趣的项目团队。目前,wasm3 已经被 wasmcloud、Siemens Opensource 等众多项目引入,作为 WebAssembly 的运行时。集团的抖音 APP 也已经接入了这款引擎,以支持相关业务。

相信随着愈加广泛的应用,wasm3 也会逐步完善,为接入它的项目带来更多的价值。

3.3 WasmEdge

WasmEdge 是一款由 CNCF(Cloud Native Computing Foundation,云原生计算基金会)托管的 WebAssembly 引擎,其命名也在告诉我们:WasmEdge 主要面向边缘计算、云原生和去中心化应用。根据 IEEE 论文数据,WasmEdge 是当时运行性能最高的 WebAssembly 运行时。但根据最新的 2023 wasm runtime benchmark 数据[2],wasmedge 的性能优势已经有所削弱。

与 wasmtime 一样,WasmEdge 也是一种编译型的 wasm 引擎,可以按照 JIT/AOT 两种模式对 wasm 指令进行编译,并最终执行。不同之处在于,WasmEdge 使用 LLVM 作为编译器后端,利用了 LLVM 出色的优化编译能力。因此,相比于使用 Cranelift 的 wasmtime,WasmEdge 生成的指令更优,执行速度更快。

除了卓越的性能表现之外,WasmEdge 的一个最为显著的特点就是社区提供丰富的扩展能力。比如,在云原生的使用场景中,很多开发者使用 JavaScript 语言开发应用。为此,WasmEdge 以 wasm 模块形式(名为 wasmedge_quickjs.wasm)提供了 QuickJS 引擎能力, 以便在 wasm 环境中执行 JavaScript 代码。在此基础上,WasmEdge 社区提供了一揽子基于 JavaScript 的能力扩展:

在云原生场景中,更多用户可以无缝切换到 WasmEdge 并运行他们原有的 JavaScript 应用,显著降低切换成本。

正是因为 WasmEdge 的优势,2022 年 11 月,Docker 正式宣布集成 WasmEdge 以提供运行 WebAssembly 应用的能力。自此,WebAssembly 和 WasmEdge 都将在 Docker 容器化的开发中扮演更加重要的角色。

3.4 wasm-micro-runtime

wasm-micro-runtime 也简称为 WAMR,与 wasmtime 一样是隶属于 Bytecode Alliance 的开源 WebAssembly 引擎项目,适用于嵌入式平台、各类 IoT 设备、智能合约和云原生等场景。名字中的 micro 也正是它的特点之一:WAMR 的二进制产物很轻量,纯 AOT 配置的产物体积只有约 50KB,非常适合资源受限的宿主。

与上述三款引擎只支持解释执行或编译后执行不同,WAMR 同步支持解释与编译两种方式执行 wasm 程序,因此兼有两种执行方式低冷启延迟、高峰值性能的优点。使用编译模式时,宿主可以选择使用 JIT 或 AOT 方式执行目标程序。与 WasmEdge 相同,WAMR 的编译器也基于 LLVM 构建。根据官方数据,JIT 或 AOT 的执行方式可以得到接近原生的速度,表现十分亮眼。而援引最新的 2023 年 wasm runtime 的性能测试数据,wamr 是运行速度最快的引擎。可见,wamr 在 2022 年度完成了行之有效的优化。

作为一款成熟的、可用于生产环境的 WebAssembly 引擎,WAMR 对社区标准支持的完整度也非常高:除了 MVP 中的特性全部支持之外,对 Post-MVP 中的 Fixed-Width SIMD、引用类型、共享线性内存、线程等提案的实现也都已正式上线。不过,对于 JS-API 中的一些接口,如 Memory.grow、Table.grow 等接口还未支持。

但是,这并不阻碍 WAMR 为非 JavaScript 环境的 WebAssembly 应用服务,这也是它主要面向的场景。目前,WAMR 已经被 Hyperledger Private Data Objects、Inclavare Containers、Fassm、Waft 等项目使用。

3.5 V8

以上介绍的都是纯粹的 WebAssembly 引擎,专用于 wasm 程序的执行,通常在 Standalone 的场景中使用。但考虑到 WebAssembly 由四大浏览器厂商联合推出支持,最初在 Web 环境中使用,JavaScript 引擎是 wasm 最早的执行引擎,因此特地介绍 V8 的一些特点。

V8 执行 WebAssembly 也是使用编译后执行的方式: 通过自身的 TurboFan 或者 LiftOff 编译后端,进行 JIT 编译后执行。根据一份 2021 年的测试数据[3],nodejs 在 Benchmark 上的性能领先于 wasmtime,可见 V8 的 WebAssembly 性能足以媲美多数专门的 wasm 引擎。

在标准支持方面,考虑到 V8 一般在 Web 或者 nodejs 环境中运行 wasm 程序,所以 V8 并不支持 WASI 标准。但是 Chrome&V8 团队作为 wasm 标准制定的主要参与者之一,V8 引擎对 WebAssembly 的 JS API、MVP 以及 Post-MVP 等核心提案都完整支持,并且实验性地支持各类处在探索阶段的提案。因此,如果想要尝试 WebAssembly 的最新提案实现,V8 引擎是一个不错的选择。

而且,通过 nodejs 的命令行工具,我们可以非常方便地使用 V8 运行包裹 wasm 模块的 JavaScript 程序。另外,nodejs 也提供了 WASI 扩展,即使是依赖 WASI 的 wasm 模块也可以借助 nodejs 运行。

4. 总结

在这篇文章中,我们先简要介绍了一门程序语言的引擎需要哪些组件,构建了一个引擎的概念模型。之后,我们重点讨论了当前社区常见的 wasmtime、wasm3、WasmEdge、wasm-micro-runtime 以及 V8 这五款引擎,分析了它们的执行方式、性能表现、标准完整程度与应用场景。除了文中所提及,还有 wasmer、wavm、fizzy 等诸多 wasm 引擎,限于篇幅,不再一一介绍。

本课程《自己动手实现一个 WebAssembly 解释器》一章将带领大家一起从零开始实现一个 WebAssembly 解释器,对引擎内部的具体实现想进一步深入了解的同学欢迎前往阅读学习。

附表

引擎 关键特征
wasmtime
  • 高性能编译
  • 字节码联盟项目
  • 应用于Serverless等场景
wasm3
  • 高性能解释器
  • 跨平台, iOS 等平台通用
  • 轻量级引擎
WasmEdge
  • 在 Docker 中集成
  • 使用 LLVM 编译后执行
  • 社区提供了非常丰富的扩展能力
wasm-micro-runtime
  • 解释 & 编译两种运行模式
  • 使用 LLVM 作为编译后端
  • 运行速度最快的引擎[2]
  • 适用于 IoT 设备等资源受限的场景
V8
  • 使用 TurboFan/LiftOff 后端编译
  • 支持各种实验性的提案
  • 适用于 Web/JavaScript 环境

表 2. 常见 WebAssembly 引擎总结

5. 参考文献

  1. Virtual Machine Showdown: Stack Versus Registers: https://www.usenix.org/events%2Fvee05%2Ffull_papers/p153-yunhe.pdf
  2. Performance of WebAssembly runtimes in 2023:https://00f.net/2023/01/04/webassembly-benchmark-2023/
  3. Benchmark of WebAssembly runtimes - 2021 Q1: https://00f.net/2021/02/22/webassembly-runtimes-benchmarks/
  4. Runtime Structure: https://webassembly.github.io/spec/core/exec/runtime.html
  5. Threaded code: https://en.wikipedia.org/wiki/Threaded_code
  6. M3 Interpreter: https://github.com/wasm3/wasm3/blob/main/docs/Interpreter.md
  7. Architecture of Wasmtime: https://docs.wasmtime.dev/contributing-architecture.html#architecture-of-wasmtime
  8. WebAssembly runtimes compared: https://blog.logrocket.com/webassembly-runtimes-compared/

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8