Lec 10寄存器分配(Register Allocation)
Lec 10 寄存器分配(Register Allocation)
配套复习课:R10 寄存器分配 + 窥孔优化(第 8–9 节)——这是项目 Phase 5 的核心 关键概念:webs、干涉图、图着色、溢出、拆分
TL;DR
- 寄存器分配根据 def-use 链和活跃区间构造干涉图,将「不能同时共用寄存器」编码为边。
- 将变量分配到 N 个寄存器等价于 N-着色;实用算法用简化、回弹着色和启发式溢出处理 NP 难问题。
- 溢出、活跃区间拆分、合并、预着色与窥孔优化共同决定最终代码质量,不能只把分配看成孤立图算法。
1. 什么是寄存器分配
程序在 def 与 use 之间须存储值,两种选择:① 定义时存内存、使用时取内存;② 定义时存寄存器、使用时读寄存器。寄存器分配就是在有限寄存器里决定存哪些值。
定理(寄存器分配的重要性)
它影响几乎每条语句,消除昂贵的内存指令、直接操作寄存器使指令数下降,很可能是影响最大的优化。寄存器比内存快(带宽约 4 倍、延迟约 3 倍),但数量少——通常 16 个整型 + 16 个浮点,且部分有固定用途(如 RSP、RBP)。
可放入寄存器的:编译器临时变量、局部标量、大常量、数组元素/对象字段(涉及别名分析)。寄存器集按数据类型分(浮点值进浮点寄存器)。
2. 两条关键思想
定理(寄存器复用的两条规则)
-
当一个临时变量死亡,其寄存器可被复用。
-
两个同时活跃的临时变量不能用同一寄存器。 :::
::: example 例题(复用 vs. 冲突)
a=c+d; e=a+b; f=e-1; (a、e 用后即死) → a、e、f 可同寄存器 r1
a=c+d; e=a+b; f=e-a; (a 在 f 处仍活) → e 与 a 不能同寄存器
当活跃变量多于寄存器:拆分活跃区间——把值 store 到内存、用时再 load 回。3. 基于 Web 的寄存器分配流程
定义(流程)
确定每个值的活跃区间(web)→ 确定重叠区间(干涉)→ 计算每个 web 保留在寄存器的收益(溢出代价)→ 决定哪些 web 得寄存器(分配)→ 必要时拆分 web(溢出与拆分)→ 给 web 分配硬寄存器(指派)→ 生成含溢出的代码。
3.1 Web
定义(Web)
起点是 def-use 链(连接定义到所有可达使用)。归并条件:一个定义与它能到达的所有使用须同一 web;到达同一使用的所有定义须同一 web(用并查集 union-find)。Web 是寄存器分配的单位——若分到寄存器 R,则其所有定义写入 R、所有使用从 R 读;若分到内存 M 同理。
3.2 活跃区间与干涉
定义(凸集、活跃区间、干涉)
集合 凸 (convex):若 且 在 到 的路径上,则 。web 的活跃区间 (live range) 是包含其所有 def/use 的最小凸指令集。两个 web 干涉 (interfere) 当其活跃区间重叠(交非空)——干涉则须存不同寄存器/内存位置。
干涉图 (interference graph):节点是 web,两 web 干涉则连边。
4. 图着色(Graph Coloring)
定义(寄存器分配即图着色)
每个 web 分一个寄存器(颜色);干涉(有边)的两节点不能同色。这是图论经典问题,NP 完全,但寄存器分配有好的启发式。
4.1 着色启发式(Chaitin 风格)
定理(N 色着色启发式)
若节点度 < N,则总能着色(给其余节点着完色后至少剩一种颜色给它)。算法:
-
反复移除度 < N 的节点,压入栈;
-
当所有剩余节点度 ≥ N,选一个节点溢出 (spill) 并移除;
-
栈空后开始着色:依次弹出节点,赋一个与其已着色邻居不同的颜色(因移除时度 < N,必有可用色)。
注意度 ≥ N 不代表不可着色,仍可能 N 色可着。
5. 溢出与拆分(Spilling & Splitting)
着色失败时两选择:① 溢出——把某 web 的值放内存,所有 def 写内存、所有 use 从内存读;② 拆分——把 web 拆成多个以减少干涉;之后重试着色。
5.1 溢出代价(Spill Cost)
选哪个 web 溢出?度 ≥ N 且溢出代价最小者。理想溢出代价是额外 load/store 的动态成本,但不可知(分支走向、循环次数未知),故用静态近似(profiling 或基于 CFG 结构的启发式)。
定义(一种溢出代价计算,偏好循环内的值)
假设循环执行 10(或 100)次: 选 spillCost 最低的 web 溢出。
例题(1 个寄存器时溢出谁?)
x 的代价 = storeCost + loadCost;y 在循环内代价 = 9·storeCost + 9·loadCost。应溢出代价较低的 x。
5.2 拆分而非溢出
定理(拆分启发式与代价/收益)
在图非 R-可着色的程序点(活跃 web 数 > N),挑一个在该点最大封闭块内未被使用的 web,在对应边处拆分(store 出去、需要时 load 回),重建干涉图并重试着色。 拆分代价 ∝ 被拆边动态跨越次数(用循环嵌套估计);收益 = 提升所干涉节点的可着色性(用干涉图中的度近似)。贪心:选收益/代价比最高的活跃区间。
6. 更多优化(Further Optimizations)
- 寄存器合并 (coalescing):对拷贝
sj = si,若 sj、si 不干涉则合并其 web(类似复制传播、减指令;但可能增大合并节点的度,使可着色图变不可着色)。 - 寄存器定向/预着色 (targeting / pre-coloring):某些变量在特定时刻须在特定寄存器(前 6 个参数、返回值),预先绑定以消除多余拷贝。
- 预拆分 (pre-splitting):活跃区间有大段"死"区时拆开(在调用点——反正要溢出;或大循环嵌套外——为循环内值预留寄存器)。
- 过程间寄存器分配 (interprocedural):跨过程边界保存寄存器昂贵(尤其小函数多时),通用调用约定低效,可按函数定制调用约定。
7. 补充(R10-1):基于 SSA 的寄存器分配(Hack 方法)
Martin 的幻灯片覆盖非 SSA IR 的技术。若用 SSA:可先 de-SSA 再分配,或直接在 SSA 上分配(Hack)。
定理(SSA 寄存器分配的优势)
-
SSA 的干涉图是弦图 (chordal) ⟹ 多项式时间最优着色;
-
所需寄存器数 = 程序点处最大同时活跃变量数;
-
把溢出决策与着色解耦。
挑战:在不破坏 SSA 的前提下插入溢出;实现多项式着色算法。
路线:① 算溢出代价 → ② 插入溢出与重载 → ③ 重建 SSA → ④ 着色 → ⑤ de-SSA。
- Belady 启发式(溢出代价):寄存器不够时,溢出"下一次使用最远"的变量。基本块内逆序计算"到最近使用的距离";跨块时合并各后继(取 MIN 是格、可用工作表;加权平均不是格、不保证收敛)。
- 插入溢出:溢出会拆分活跃区间、降低寄存器压力;目标是使同时活跃变量数 ≤ 物理寄存器数(SSA 下即 #活跃 = #所需寄存器)。
- 重建 SSA:重载也是变量的定义(从内存载入)⟹ 需对重载重跑 SSA 构造(涉及 memory phi)。
- 着色实现:Hack 的算法可不显式构造干涉图做最优着色,但更复杂、难加寄存器约束;建议先用 ILP 求解器(图着色 NP 难,自制暴力差),ILP 易改目标以表达亲和性(φ 实参偏好同寄存器、三地址源/目的偏好同寄存器、跨调用的活跃变量偏好 callee-saved)。开源求解器:GLPK、Cbc、HiGHS。
工程提示:用极少寄存器(如 3 个)压力测试分配器以暴露边界 bug;动手前先想清楚、充分注释不变式、必要时加断言、用"小黄鸭"或队友复核。
8. 补充(R10-2):窥孔优化(Peephole Optimization)
定义(窥孔优化)
汇编层面的优化:取一小段汇编,变换为更优的等价段;常可与代码生成同时进行(第一遍就尽量发好代码)。
应当避免发出的冗余:movq %r8,%r9; movq %r9,%r8、pushq %rax; popq %rax、连续两次写同一寄存器等。
实用 x86 技巧:
- 清零寄存器
xor %reg,%reg(比movq $0,%reg代码更小、利 icache;寄存器重命名器能识别该模式直连硬连线零)。 - 直落 (fallthrough):循环体常被执行,尽量"落入"循环体,使后续指令在 cache 中已热。
- 省帧指针
-fomit-frame-pointer:Decaf 可执行很少分发、无变长数组,可把%rbp当通用寄存器用。 - 红区 (Red Zone):函数可访问其初始栈指针下 128 字节而无需分配栈空间;对叶子调用,若需 < 128 字节可省去栈操作(非叶子调用则危险)。
- 2 的幂 / lea / cmov:移位代替乘除、
lea做地址算术与轻量算术、cmov消分支。 - 常量除法的"魔数 (magic number)":用乘以魔数 + 移位代替除法(Hacker's Delight / LLVM DivisionByConstantInfo);魔数越小可能乘法越快,取最小者。
我的理解
高质量分配器处理的是带有调用约定、指令限制和移动代价的资源问题,而不是纯粹图着色。因此溢出代价、coalescing 与窥孔优化应在同一个性能目标下协同设计。
9. 寄存器分配与窥孔优化小结
- 核心:把 def 到 use 之间的值尽量放寄存器;同时活跃者不能共用,死后可复用。
- Web(用 union-find 由 def-use 链构造)→ 干涉图(活跃区间重叠连边)→ 图着色(NP 难,用"移除度<N 入栈、回弹着色"启发式)。
- 不够色时溢出(按 spillCost 选,偏好循环内)或拆分(按收益/代价比);coalescing/pre-coloring/pre-splitting/过程间分配进一步优化。
- SSA 下干涉图为弦图,可多项式最优着色,溢出与着色解耦;实践用 ILP + Belady 启发式。
- 窥孔优化在汇编层清理冗余并套用 xor 清零、红区、lea/cmov、魔数除法等技巧。