一个栈式 VM,最怕的不是栈,而是搬栈。

这篇 2014 年旧文有个很硬的实验:作者受 F18、x87 rotating stack 和 Forth 启发,把虚拟栈限制在 8 个槽,直接映射到 x86-64 的 r8-r15。push、pop 不搬数据,只改一个模 8 的“栈顶状态”。

反常点在这里:它不是靠更聪明的运行时调度来省指令,而是提前生成 8 份 primitive 变体。栈顶在哪个寄存器,就跳到对应版本。

文首那个小更正也很有味道。NEXT 分发序列里,旧版 MOV EAX, [RDI] 被浪费编码成 14 字节,修正后是 9 字节。语义没变,机器码形状变了。底层实验经常就卡在这 5 个字节上。

这个原型做了什么

这不是 SBCL 官方特性,也不是完整高性能 VM。它是作者拿 SBCL unsupported internals 搭出来的原型,展示汇编生成、基础 primitive、控制流、call/ret,以及从 SBCL 进入 VM 的 FFI 入口思路。

核心设计可以压成一张表:

组件做法直接后果
虚拟栈8 个 32 位槽固定放进 r8-r15数据常驻寄存器,栈深被硬限制
push/pop只改变编译期模 8 栈指针少搬数据,但依赖状态复制
primitive每个操作生成 8 种栈指针版本减少指令,换来代码膨胀
分发direct threading,NEXT 读 32 位 offset 再跳转bytecode 更紧,间接分支仍要付成本

普通 direct threaded VM 常让虚拟指令直接指向 primitive 地址。作者不想在 bytecode 里塞 64 位地址,于是改用 32 位 offset:NEXT 从虚拟 IP 读 offset,加上 primitive code base,再跳过去。

难点是当前旋转栈状态也要跟着走。

他的办法很粗:同一个 primitive 的 8 个版本按固定间隔排好。栈指针是 0,跳第 0 个版本;栈指针是 3,跳第 3 个版本。dup 调整栈顶,drop 调整栈顶,add/sub 在对应寄存器上算完,再进入下一个 NEXT

这套东西的启发来自 F18、x87 和 Forth,但不能倒过来说它已经证明这条路线更优。F18 和 x87 更像火种,不是证据。

真正的账:少搬数据,换更多形状

我更在意的是这笔账。

它省掉了 push/pop 的数据移动。代价也摆在桌上:primitive 复制 8 份,代码要 padding 对齐,NEXT 分发变复杂,间接分支仍可能是瓶颈。现代 CPU 对“下一跳去哪”很敏感,BTB、分支预测、I-cache 都会来收税。

这也是它不能被吹成通用寄存器分配方案的原因。

8 槽旋转栈适合栈深可控、操作模式稳定的栈式代码。换成复杂通用语言,问题会马上变样:溢出怎么办,长生命周期值怎么办,调用边界怎么办,调度策略怎么办。

和另一条路线相比,它的取舍更清楚:

路线优点硬约束
解释器栈放内存简单、可移植、调试容易push/pop 搬运多,寄存器利用差
JIT 做寄存器分配适用面广,优化空间大实现成本高,编译器工程重
8 槽旋转栈低搬运,primitive 可直接落到寄存器栈深受限,代码膨胀,隐式状态难管

所以这篇文章最不该被读成“一个更快 VM 的发布”。原文没有给出能支撑大结论的性能结果。它目前只能说明:这个想法可以被快速做成机器码原型,并且能看见它的收益和代价。

对编译器和 VM 实现爱好者,动作很具体:可以读它的 primitive 布局、NEXT 编码和栈指针状态复制,拿来训练自己看底层取舍。别急着照搬。

对熟悉 Lisp/SBCL、Forth 或汇编优化的开发者,更现实的动作是把它当实验模板:如果你想验证一种分发策略、栈布局或指令编码,SBCL 的内部汇编器能让你少搭很多脚手架。但要接受一个前提:unsupported internals 会变,坏了没有稳定承诺。

SBCL 的价值,不在“神”,在能快试

“工欲善其事,必先利其器。”这句话放在这里不空。因为主角不是 SBCL 多强,而是这类运行时还保留了一种稀缺能力:把想法直接落到机器码。

你可以在 Lisp 环境里生成汇编,反汇编,改编码,看 byte vector,再生成一版。这个循环很短。短到足以让一个本来只停在纸面上的 VM 分发策略,变成能跑、能拆、能失败的东西。

很多底层想法死得早,不是因为错,而是因为试一次太贵。写汇编器、装载器、调试入口、调用边界,工程前戏可能比想法本身还重。

SBCL 在这里像一块汇编面包板。不好看,也不稳当,但插上线就能看电路有没有反应。

扯远一点,早期 PC、Unix 工具链、浏览器脚本环境都干过类似的事:降低试错门槛。历史不完全一样,但重复的是同一个结构——工具一旦让实验成本下降,边缘想法就会变多,筛选也会更快。

这篇旧文的价值就在这儿。它不证明旋转栈 VM 一定赢。它至少表明,好的工具链会把“我猜这样能行”压缩成“生成一段机器码试试”。

接下来真正该看什么也很具体:不是有没有更漂亮的叙述,而是这类原型能否补上性能测量、栈溢出策略、分支预测影响、代码尺寸控制,以及 SBCL 内部接口变化后的维护成本。

如果这些账算不清,它就是一个漂亮实验。如果这些账能被逐项压住,它才有资格往工程方案走。