Hotdry.

Article

WebAssembly非严格栈机:混合执行模型对编译器优化的影响

从宏观执行模型层面分析WebAssembly的混合栈机特性,解析其对编译器后端优化的深层影响。

2026-04-28compilers

当我们谈论 WebAssembly 的执行模型时,官方文档和大多数技术文章都会将其描述为「基于栈的抽象结构化机器」。然而,这个描述过于简化,实际上掩盖了一个更为复杂的混合执行模型。理解这一模型对于编译器开发者至关重要,因为它直接影响代码生成的策略和优化空间。

栈机与寄存器机的本质差异

在深入 WebAssembly 之前,有必要厘清栈机和寄存器机的根本区别。传统栈机的操作数隐式地从栈顶获取,运算结果同样压回栈顶。例如,一条加法指令会弹出栈顶两个值,计算后将结果推回,整个过程不需要显式命名任何中间结果。寄存器机则完全不同,它操作的是一组具名寄存器,每条指令明确指定读取哪两个寄存器并将结果写入哪个寄存器。这种显式命名带来了一个关键问题:寄存器分配器必须通过活跃性分析来确定某个寄存器在何时不再被使用,从而可以回收重用。

活跃性分析是现代优化编译器的基础设施。在 SSA(静态单赋值)形式出现之前,寄存器机器需要复杂的活跃区间计算;即使在 SSA 形式下,虽然每个值只被赋值一次,活跃性信息仍需持续维护。这意味着流式编译器(如 Firefox 的 Wasm 基线编译器)在缺乏完整程序视图的情况下,难以生成高质量的机器码。

WebAssembly 的混合执行模型

WebAssembly 的设计者最初希望保留栈机的简洁性和紧凑编码,同时又需要一种机制在函数内部传递数据。这就是 locals 机制的由来 —— 函数级别的可变量,生命周期贯穿整个函数。由于 WebAssembly 的块(block)不能接收参数,块外部的数据必须通过 locals 传递,这导致了如下代码模式:

(func (result i32)
  (local i32)
  (local.set 0 (i32.const 42))
  (block (result i32)
    (local.get 0)
  ))

这种设计使得 WebAssembly 在语义层面更接近寄存器机而非纯栈机。关键问题在于:locals 是可变的,且其生命周期覆盖整个函数。这两个特性直接破坏了 SSA 形式和活跃性分析的可能性。换言之,WebAssembly 实际上是一个「没有活跃性分析的寄存器机」,甚至还不是 SSA 形式 —— 这两项编译器优化的核心工具都被排除在外。

从历史角度看,这一设计并非刻意为之。WebAssembly 最初并非作为字节码设计,而是作为 asm.js 的更高效二进制表示,定位更接近源代码而非虚拟机指令集。后来演变為寄存器机器,直到最后时刻才切换到基于栈的编码方式彼时 locals 概念已经深深扎根于规范之中。

对编译器后端优化的实际影响

这一混合模型对编译器后端优化产生了具体而深远的影响。当将 WebAssembly 编译到目标机器架构时,如果不做额外的活跃性分析,所有 locals 在函数入口时就会被标记为「在使用」,直到函数结束才释放。这意味着即便某个局部变量在函数中途就再也不被使用,编译器仍会为其保留寄存器资源,造成不必要的寄存器占用。

流式编译场景下这个问题更为突出。基线编译器需要在快速编译和代码质量之间取得平衡,但没有完整程序视图就无法重建精确的活跃性信息。结果是流式编译器生成的代码往往比经过完整优化的 AOT 编译器质量低得多 —— 而这种质量损失完全是由于 WebAssembly 执行模型的局限导致的,并非不可避免。

现代 WebAssembly 引擎通过内部转换来弥补这一缺陷。以 V8 和 Wasmtime 为例,它们在将 WebAssembly lowering 到机器码之前,会先执行活跃性分析和寄存器分配,将无限数量的虚拟寄存器映射到有限的物理寄存器,超出容量的部分进行溢出处理。这本质上是在引擎内部重新构建本应在字节码层面就有的信息。

多值提案的改进方向

WebAssembly 的多值提案(Multi-Value)为解决上述问题提供了一个可行的方向。该提案允许块拥有参数,函数返回多个值,从而可以逐步淘汰 locals 机制。具体而言,循环计数器可以作为参数传入循环头部,跳转时必须将新的计数值放在栈上 —— 这实际上充当了 phi 函数的功能,但概念上更加简洁。由于无法获取 locals 的地址,也就不需要类似 C 语言中的 alloca 机制。

这种设计下,WebAssembly 将真正成为严格的栈机,自动满足 SSA 形式甚至是严格 SSA 形式的要求。这不仅简化了规范本身,也使得生成 WebAssembly 的编译器能够将更多关于源程序的知识编码到字节码中。虽然优化编译器可能不会因此生成更好的代码,但编译器的实现将大幅简化,流式编译器也能获得显著更好的代码质量。

工程实践中的应对策略

对于编译器开发者而言,理解 WebAssembly 的混合执行模型是优化工作的大前提。在生成 WebAssembly 时,应优先产生符合栈机语义的结构,将复杂的寄存器分配留给引擎完成。同时,应该有意识地利用 locals 的结构,为后续的活跃性分析创造条件。具体参数上,建议将热点函数的局部变量数量控制在物理寄存器可容纳的范围内(通常为 8 至 16 个),以减少溢出带来的性能损失。

从更宏观的视角看,WebAssembly 的执行模型提醒我们:语言设计的选择会深刻影响后续编译优化的空间。栈机的简洁性带来了紧凑的二进制编码和简化的验证逻辑,但牺牲了显式活跃性信息的表达能力。这种权衡并非错误,而是不同目标之间的取舍。理解这一取舍,正是写出高效 WebAssembly 编译器的关键。

compilers