开发者解析如何将字节码转换为 SSA 形式
来源标题:SSA from Bytecode: Imperative Pearl
开发者撰文解释如何把 CRuby 的 YARV 栈式字节码转换为 ZJIT 使用的高层 SSA 中间表示。核心逻辑是把每条指令隐式的栈输入输出映射为 SSA 值,例如将 push/add 序列转成 const 与 add 三个 SSA 值。完整的 add_iseq_to_hir 函数约 2000 行,主要用于处理大量特定 opcode,但核心部分很小。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
Ufuk 评论说,他喜欢我在 Aaron 和我一起做的直播中对从字节码构建 SSA 的描述。
在拍摄那场直播——ZJIT 内部原理入门——的时候,我有点担心大家会被我们那个 1900 行的 add_iseq_to_hir 函数吓退。这个函数负责把 CRuby 中 YARV 虚拟机的基于栈的字节码,转换成供 ZJIT 分析和优化的高层 SSA IR。
它把大致长这样的字节码:
push 3
push 4
add
转换成 SSA 形式,给每个栈槽大致分配一个 SSA 值:
v0 = const 3
v1 = const 4
v2 = add v0, v1
我们的函数有 2000 行,是因为它要处理大量特定的 opcode 以及这些 opcode 的各种奇怪细节,但核心部分其实很小。下面就是那个核心。
字节码假设自己运行时是在一个栈上操作。这意味着每个 opcode 都有隐式的栈输入和隐式的栈输出。比如 push 3 的栈输入为空,栈输出为一个。相比之下,add 有两个栈输入和一个栈输出。有些指令如 nop 则两者都为空。据我所知,所有指令最多只有一个输出。
这些数量在查看字节码时就是已知的:我们可以静态地推断出每个 opcode 有多少个输入和输出。有些是可变参数的,比如 send(调用),但它们会在附近的字段里带上参数个数。
让我们通过在每个指令执行后添加栈的视图,来模拟第一个例子在 VM 上的运行过程。栈将显示为一个从左到右的列表,左边是栈底,右边是栈顶。
; []
push 3 ; [3]
push 4 ; [3, 4]
add ; [7]
由于输入数量和输出数量是静态的,当 ZJIT 读取字节码时,我们可以在编译期就把其中与栈相关的部分跑完。
我们其实不需要知道栈上的值是什么;我们只需要追踪它们来自哪里。而说到“它们来自哪里”,我的意思是:我们给每条指令命名,并用指令的名字来引用它的输出。我很喜欢论文《Design of the Java HotSpot™ Client Compiler for Java 6》(PDF)中的说法:
一条加载或计算值的指令同时代表了操作和它的结果,因此操作数可以表示为指向先前指令的指针。
下面是上述例子的样子。除了运行时的栈之外,我还会写出 SSA 指令,以表示我们正在生成新代码:
; []
push 3 ; v0 = const 3
; [v0]
push 4 ; v1 = const 4
; [v0, v1]
add ; v2 = add v0, v1
; [v2]
这很棒!我们把基于栈的字节码转换成了指令可以直接引用其输入的形式。不过你可能会注意到,v0 和 v2 最终都落在同一个栈槽(第一个元素)里。这就是为什么我之前说“给每个栈槽大致分配一个 SSA 值”。
两者的区别在于时间。v0 曾经在操作数栈上,但现在在那里的是 v2。这大概也是为什么我把 SSA 描述为“把时间推进到 IR 中”。
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。