Register Allocation:寄存器分配¶
约 1256 个字 3 张图片 预计阅读时间 4 分钟
这一部分作用不言而喻。我们的目标是:
- 产生正确的代码,将变量分配到 <=k 个寄存器中
- 最小化 load 与 store 数量,并且最小化 spill 到内存中的空间
- 分配过程要高效
有两种方法:
- 图着色法:效果好,效率稍差
- 线性扫描:效率更高,且有着相近的效果
下文重点介绍图着色法。
1. 通过“简化”来着色¶
值得一提的是寄存器分配是个 NP 完全问题,所以这里的图染色法也是一个近似,且是线性时间的近似。其基本流程是 build -> simplify -> spill -> select。
1.1 Build¶
就是构建干扰图。
1.2 Simplify¶
使用一种简单的假设来着色。大前提:机器的寄存器数量为 K。
- 假设干扰图 G 包含一个节点 m,这个节点的邻居少于 K 个
- 则通过删掉这个节点获得新图 G' = G - {m}
- 如果 G' 可以被着色,则 G 也可以被着色
- 这实际上形成了一个基于栈(或者说递归)的着色算法
- 重复去除邻居数量小于 K 的节点,都把它们 push 到栈中
- 每次删除都会减少一部分节点的邻居数量,提供更多 simplify 的机会
- 直到图中只剩下 significant degree 的节点(即邻居数量 >= K 的节点)或者什么都不剩下
1.3 Spill¶
如果图中只剩下 significant degree 的节点,则需要进行 spill。
- 可能选择一些节点,让其值放进内存中而不是寄存器中
- Optimistic Coloring
- 一个乐观的估计:被 spilled 的节点不与剩下的节点相互干扰
- 从而可以被移除并 push 到栈中,然后继续 simplify
- 这些节点可能是 potential spill,后续可能会发现其也是可以放进寄存器的
1.4 Select¶
这一步为节点着色。
- 从空图开始,一步一步把栈顶的节点 pop 出来并分配颜色
- 如果 pop 出的这个节点是刚才 spill 操作 push 进去的,这个节点不能保证被着色
- 如果说这个节点的邻居数量小于 K 个节点,那就可以被着色,也就不是一个 actual spill
- 反之就是一个 actual spill,不为其上色而是 continue
如果在 select 阶段,不能为某些节点上色,则程序需要被重写:
- 每次用这些 actual spill 的节点之前,需要从内存中 load 其值
- 每次用这些 actual spill 的节点之后,需要 store 其值到内存中
这样之后,一个 spill 的变量会转换为多个活跃性较短的变量,这些变量也可能会与其他变量发生干扰。
- 这就需要重新构建干扰图,重新进行 simplify 和 select
- 直到 simplify 可以在没有 spill 的情况下完成
- 这可能需要多次迭代,一般情况下一到两次就可以完成
2. 合并 Coalalescing¶
2.1 定义¶
如果 MOVE 指令的两边没有干扰,这个 MOVE 就可以被消掉。这样,src 和 dst 的节点就被合并成一个新的节点,边就是 src 和 dst 的边的并集。
为什么合并?因为合并可能带来 coloring 的机会。
但是合并也存在问题:
- 新的节点的 edges 更多
- 原来的 k-colorable 图可能就不是 k-colorable 了
所以我们需要进行保守的合并,也即检查确认安全后合并。安全的意思就是合并操作不会把原来的图变成一个非 k-colorable 的图。
如何检查安全性呢?有两个准则:Briggs 和 George。
2.2 Briggs 准则¶
定义 significant degree 的节点为满足 degree >= k
的节点。
如果合并后的节点拥有少于 K 个 significant degree 的邻居,则合并是安全的。
2.3 George 准则¶
两个节点 a 和 b,如果对于 a 的每个邻居 t:
- 要么 t 已经与 b 干扰
- 要么 t 是 insignificant degree 的节点(即
degree < k
)
则合并是安全的。
2.4 包含合并的寄存器分配过程¶
- Build
- 构建干扰图
- 每个节点分为 move-related 和 non-move-related
- move-related 节点是指那些通过 MOVE 指令连接的节点
- Simplify
- 移除邻居数量小于 K 的 non-move-related 节点
- Coalesce
- 合并 move-related 节点
- 如果合并后某个节点不是 move-related,就可以进行下一轮的 simplify
- 一直重复 simplify 和 coalesce,直到图中只剩下 significant degree 或者是 move-related 的节点
- Freeze
- 如果没有使用 simpilfy 或 coalesce,我们去看 low degree 的 move-related 节点
- 实际上放弃了合并的希望
- 导致这个节点(可能还有与其相关的其他节点)被认为是 non-move-related
- 然后再进行 simplify 和 coalesce
- 如果没有使用 simpilfy 或 coalesce,我们去看 low degree 的 move-related 节点
- Spill
- 如果没有 low-degree 节点,就选择一个做 potential spill
- Select
- 选择栈顶的节点进行着色
- 如果是 potential spill 的节点,则不着色而是继续
3. 预填色 Precolored Nodes¶
有些寄存器是 special purpose 的:
- 参数寄存器
- 帧指针
- 返回值等等
这些寄存器是被预先填色的:
- 一个颜色只能有一个 precolored node
- precolored nodes 之间互相干扰
需要注意的是:
- 可以为一个普通临时变量分配一个 precolored node 的颜色,只要它们之间没有干扰
- 不可以 simplify precolored nodes
- precolored nodes 不能被 spill
- 前端有义务将其活跃度缩短
- 给个例子
区分 caller-saved 和 callee-saved 寄存器
活跃度不跨越 procedure call 的寄存器一般是 caller-saved 寄存器;需要跨越 procedure call 的寄存器一般是 callee-saved 寄存器。