从一万英尺高空俯瞰 MIR 优化

MIR 是 Ion(SpiderMonkey 的优化编译器后端)中使用的中间表示(IR)。MIR 由 WarpBuilder 生成,然后通过一系列传递进行优化。(MIR 也用于编译 WebAssembly 代码,但本文档重点关注 JavaScript 编译)。

这是截至 2021 年 2 月所有 MIR 传递的快速总结。斜体传递是经典的优化,在编译器教科书中可能会被广泛介绍。非斜体传递要么是 JS 特定的,要么过于简单,不适合介绍。

可以使用 iongraph 可视化这些传递之后 MIR 的状态。

BuildSSA

静态单赋值 是一种 IR 形式,其中每个值都恰好定义一次。它对优化具有几个很好的特性。注意:SSA 是我们拥有 phi 节点的原因。

剪枝未使用的分支

顾名思义:剪除从未执行的分支。

折叠空块

一个简单的清理传递,通过将空块折叠到其后继块中来消除只有一个前驱和一个后继的空块。

折叠测试

简化条件操作生成的代码。参见此处注释

拆分关键边

在后续传递中,我们可能会选择移动代码。在此准备工作中,此传递在 关键边 上添加空块,以便我们有一个安全的地方放置这些指令。

重新编号块

此传递实际上只是重新分配块号。

消除 Phi

在上述一些优化之后,我们的一些 phi 节点可能是像 b = phi(a,a) 这样的东西,这是冗余的。此传递清理了这些内容。

标量替换

如果一个函数分配并使用一个对象,但我们可以 证明该对象从未逃离该函数,那么我们有时可以通过单独跟踪对象的每个组件(C/C++ 中的字段;JS 中的槽/元素)来完全避免分配。

应用类型

每种类型的 MIR 节点都有一个 TypePolicy 定义它接受哪种类型的输入。如有必要,此传递将插入(可能不可靠的)转换以保证类型正常工作。

别名分析

别名分析 确定两个指令是否可能使用/修改相同的内存。如果它们确实如此,那么它们不能彼此重新排序,因为这可能会更改程序的语义。

GVN

全局值编号 是一个经典的优化,用于查找多次计算相同值的地方,并消除冗余。

LICM

循环不变代码移动 查找循环中每次都会计算相同值的指令,并将它们提升到循环之外。

Beta

这是为了准备范围分析。这种特定方法的范围分析是 源自 Gough 和 Klaren 的一篇论文

范围分析

一个经典的优化,它确定定义可以取的可能值的范围。用于实现许多后续传递。

De-Beta

删除不再需要的 beta 节点。

RA 检查 UCE

检查范围分析是否使任何代码有资格进行不可达代码消除。

截断双精度浮点数

如果范围分析表示可以,则将双精度浮点数算术运算强化减少为整数算术运算。

下沉

如果我们计算一个值,该值仅在某些路径上使用,并且如果其他路径之一失败,我们可以恢复该值,那么我们可以将该变量的计算推迟到我们确定需要它为止。更多详细信息请参见此处

删除不必要的位运算

删除不执行任何操作的按位算术运算符(例如,对整数输入的 x | 0)。

折叠线性算术常量

a + constant1 + constant2 折叠为 a + (constant1+constant2)

有效地址分析

添加此功能是为了确保我们为 asm.js 中的内存访问生成良好的代码。

DCE

死代码消除 删除其结果永远不需要的指令。

重新排序

在块内重新排列指令以减少中间值的生存期并减少寄存器压力。这是 指令调度 的一个相对简单的版本。

使循环连续

重新排序块,以便循环中的所有块都以一个连续的块生成,这有利于缓存局部性。

边缘情况分析(晚期)

在代码停止移动后检查边缘情况的地方。目前用于检查某些指令是否需要处理负零。

边界检查消除

一个经典的优化,如果我们可以在编译时证明边界检查不会失败,则消除边界检查。

折叠带解箱的加载

加载 NaN 封箱值然后解箱它,如果我们同时执行这两个操作,可能会稍微更高效。

添加 KeepAlive 指令

当我们访问对象的槽或元素时,我们必须确保对象本身不会被 GC 收集。参见 bug 1160884

生成 LIR

优化完成后,我们将 IR 从 MIR 降级到 LIR 以进行寄存器分配和代码生成。

分配寄存器

程序可能拥有比硬件寄存器更多的变量,因此我们必须决定哪些值在何时存储在哪些寄存器中。这是一个研究得很充分的领域