大家都说 WebAssembly 是栈机器。Wikipedia 这么写,Wasm 设计文档也这么说。
但一个很反常的细节是:它连传统栈机器最基本的“搬弄栈”的能力都很弱。没有常见的 dup,没有 swap,没有 over。主要能做的栈操作,几乎只剩 drop。
这就有意思了。一个被长期归类为栈机器的东西,偏偏不太允许你像栈机器那样写程序。
栈在 Wasm 里,更像编码,不像完整机器模型
先把概念压短。
寄存器机器,是显式写位置。比如 a = b + c,指令里写清楚取哪几个寄存器、结果放哪。
栈机器,是靠顺序隐式取值。push 2、push 3、add,add 默认拿栈顶两个值。
简单表达式里,两者差别不大。比如算 2 3 + 5 7,栈式写法很自然:先压 2、3,乘;再压 5、7,乘;最后加。
麻烦出在复用。
比如已经算出 x,现在要算 x x x。传统栈机器会用 dup 复制栈顶,再配合 swap 调整顺序。JVM bytecode 里就有 dup、pop、swap 等操作。当然,JVM 也不是“纯栈机器”,它同样有 local variable 指令,比如 iload、istore。
Wasm 的情况不同:
| 模型 | 怎么引用值 | 栈重排能力 | 复杂复用靠什么 |
|---|---|---|---|
| 寄存器机器 | 显式寄存器索引 | 不依赖栈 | 寄存器/变量 |
| 传统栈机器 | 程序顺序隐式决定 | dup、swap、over 等 | 栈操作 |
| JVM | 操作数栈 + locals | 较完整 | 栈操作与 locals |
| Wasm | 操作数栈表达输入输出 | 基本只有 drop | locals |
所以原博客的核心判断很清楚:Wasm 确实用操作数栈表达指令输入输出,但它不像 Forth 或 JVM 那样,把栈重排当成主要编程能力。
二进制 Wasm 用逆波兰式,当然可以用栈求值。但这更像一种紧凑编码。文本 Wasm 甚至可以写成类 Lisp 的前缀结构。换句话说,栈在这里更多是“表达式序列化方式”,不是完整计算世界观。
误导不在名字,而在经验迁移
我更在意的不是 Wasm 到底该不该叫栈机器。技术圈太爱为名词开庭,但工程里真正伤人的,往往不是名字不准,而是名字让你带错模型。
如果你带着 JVM 或 Forth 的经验来手写、调试、优化 Wasm,就很容易踩坑。你以为可以靠栈操作优雅复用中间值,结果发现路很窄。公共子表达式消除、expr^2 变成 expr * expr 这类事,一旦涉及复用,通常就要引入 locals。
这不是说 Wasm 性能差。现代编译器可以把它转成 SSA,中间格式怎么编码,不必直接决定最终机器码质量。Wasm 当初选择这种设计,也有现实好处:格式紧凑,校验清晰,解释器实现容易,浏览器和运行时更愿意采用。
少即是多,有时是好设计;少到让老经验失效,就该换脑子。
multi-value 扩展后来改善了块与栈交互的限制,比如控制流块能返回多个值。但它没有把 Wasm 变成传统意义上的“栈语言”。真正的分水岭仍然在 locals、优化器和语义设计上。
这里有点像早期铁路借用马车时代的词。车厢、驿站、线路都像旧词,但调度逻辑已经变了。Wasm 也一样,名字沿用了栈机器的影子,骨架却更接近“用栈式编码包装复合表达式的寄存器机器”。
“名不正则言不顺”放在这里不算夸张。因为这个标签会决定你怎么读指令、怎么写生成器、怎么设计优化 pass。
Wasm 没有背叛栈。它只是没打算成为你熟悉的那种栈机器。
