Archipelago

Lec 22寄存器分配 II(Register Allocation II)——线性扫描与图着色

讲义完成更新于 2026-09-16

Lec 22 寄存器分配 II(Register Allocation II)——线性扫描与图着色

与 L21 共用同一份 slides(《Register Allocation (+ Liveness, Dead Code Elimination)》,分配算法部分 credit Stanford CS143)。L21 打好了地基(活跃性分析、活跃区间、寄存器分配问题形式化),本讲落实 slides "Goal" 提出的两种分配算法:线性扫描 (Linear Scan) 与 图着色 (Graph Coloring)。结合 index.md 推荐的经典论文(Chaitin 1982、Poletto–Sarkar 线性扫描、George–Appel 1996)补充算法细节。

TL;DR

  • 图着色分配从活跃性构造干涉图,用简化、选择、溢出与 move coalescing 寻找高质量分配,但编译成本较高。
  • 线性扫描按活跃区间起点排序,维护 active 集并回收到期寄存器;它接近线性时间,更适合受编译延迟约束的 JIT。
  • 两种算法都必须正确插入溢出 load / store,遵守固定寄存器与 caller / callee-saved 约定,并保证每个程序点上值的位置一致。

1. 回顾与目标

L21 已得到:把栈式 IR 转成三地址码后,用活跃性分析算出每个变量的活跃区间 (live interval);不能共用寄存器的变量来自活跃区间重叠。

定义(本讲目标)

探索两种寄存器分配算法:

  • 线性扫描 (Linear Scan)——快、简单,适合 JIT;

  • 图着色 (Graph Coloring)——质量高、更通用,适合 AOT/优化编译。

两者都建立在 L21 的活跃性结果之上。

回顾约束优化视角:假设 load_local 0 的 y 在 rdi,约束(t1/t2/t3 用 rdi、return t4 用 rax)下最小化内存访问与寄存器间 mov,求得最优分配 t1..t3→rdi, t4→rax。线性扫描与图着色就是求解这类问题的两种实用算法。


2. 干涉与干涉图(Interference)

定义(干涉 Interference)

两个变量干涉,若它们在某程序点同时活跃(活跃区间重叠)——则它们不能分配到同一寄存器。

定义(干涉图 Interference Graph)

结点 = 变量(虚拟寄存器/临时值);无向边 = 两变量干涉。给 k 个寄存器分配 = 用 k 种颜色给干涉图着色,使相邻结点异色。这正是图论的 k-着色问题。

用 L21 的例子:d=e+f 处 {e,f} 同时活跃 → e 与 f 干涉,连边;e=d+a 处 {a,b,c,d} → a,b,c,d 两两干涉。把所有重叠关系画成图,就得到干涉图。


3. 图着色寄存器分配(Graph-Coloring)

经典的 Chaitin / Chaitin-Briggs 算法。核心定理:

定理(度数 < k 必可着色)

若干涉图中某结点的度数(邻居数)小于可用寄存器数 k,那么无论其邻居如何着色,总能给它留出一种颜色。

于是可移除这种低度结点(连同它的边),递归着色剩下的图,最后再把它放回并染色。

3.1 算法步骤(Chaitin-Briggs)

定义(图着色分配流程)

  • Build(建图):由活跃性分析建干涉图;

  • Simplify(化简):反复移除度数 < k 的结点,压入栈;

  • Spill(溢出候选):若只剩度数 ≥ k 的结点,选一个标记为潜在溢出并移除(也压栈),继续 simplify;

  • Select(着色):从栈逐个弹出结点放回图,分配一个与邻居不冲突的颜色;若某潜在溢出结点放回时无可用颜色 → 实际溢出 (actual spill);

  • Spill code(溢出代码):为实际溢出的变量插入存/取内存指令,然后重建图重跑。 :::

3.2 溢出(Spilling)

寄存器不够时,把某变量放到内存(栈帧槽),在每次使用前 load、定义后 store。选谁溢出是启发式:通常选溢出代价低 / 度数高者(如使用次数少、活跃区间长、不在循环内的变量)。溢出降低图的度数,使其变得可着色。

3.3 寄存器合并(Coalescing)

定义(Coalescing 合并)

对于 a = b 这样的复制 (move),若 a 与 b 不干涉,可把它们合并成一个结点分配到同一寄存器,从而消除这条 mov(这正是 L21 里"为什么这么多 mov"的解药)。

风险:合并会提高结点度数、可能让图变得不可着色。George–Appel 的迭代寄存器合并 (Iterated Register Coalescing, 1996) 用保守判据(Briggs/George)只在不破坏可着色性时才合并。

定理(图着色的复杂度)

一般 k-着色是 NP 完全的,故实用分配器用启发式(Simplify/Coalesce/Freeze/Spill 的迭代)。质量高但编译较慢——适合 AOT/优化编译。


4. 线性扫描寄存器分配(Linear Scan)

Poletto–Sarkar 的算法,为快速编译(JIT)而生:不建干涉图,只在一条线性扫描中处理活跃区间。

定义(线性扫描)

把每个变量近似为一个活跃区间 [start, end](L21 定义的 live interval)。按 start 排序,从前往后扫描;维护一个按 end 排序的活跃区间集合 (active list):

  • 遇到新区间,先expire(淘汰)所有 end 已早于当前 start 的旧区间,释放它们的寄存器;

  • 若有空闲寄存器,分配给新区间,加入 active;

  • 若无空闲寄存器 → 在新区间与 active 中 end 最晚者之间选一个溢出(通常溢出 end 更晚的那个)。 :::

::: theorem 定理(线性扫描的取舍)

  • 优点:近似线性时间、实现简单、内存开销小 → JIT 首选(早期 HotSpot client、V8 等);

  • 缺点:把活跃范围粗化为单一连续区间,精度低于图着色,可能多溢出;有诸多改进(区间带"洞"的版本、second-chance、SSA 上的线性扫描)。 :::


5. 两种算法对比

::: theorem 定理(Linear Scan vs Graph Coloring)

维度线性扫描图着色
数据结构排序的活跃区间干涉图
编译速度快(近线性)慢(NP 完全 + 启发式)
代码质量较好更好(更少溢出、可合并)
消除 move有限coalescing 强
适用JIT、快速开发循环AOT、优化编译

呼应 L18 的"何时编译":JIT 要省编译时间 → 线性扫描;AOT 求极致代码 → 图着色。MITScript 项目里可先实现线性扫描跑通,再视需要上图着色。


6. 一致性与正确性要点

承接 L21 的寄存器一致性:

::: theorem 定理(分配必须遵守的约束)

  • 每个程序点每个变量唯一位置;同一变量可在不同点位于不同寄存器/内存;

  • 不同时活跃的变量可共用一个寄存器(这正是分配能"省"的根源);

  • 必须尊重 ISA 约束:结果固定落某寄存器的指令、caller/callee-saved 约定(跨 call 要保存的寄存器需在调用前后 push/pop 或避免使用);

  • 溢出代码必须在每次 use 前 load、def 后 store,保证语义不变。 :::


7. 与参考资料的对应(补充)

  • 图着色:Chaitin《Register Allocation & Spilling via Graph Coloring》(1982);改进与合并见 George & Appel《Iterated Register Coalescing》(1996)——均在 index.md 列出。
  • 线性扫描:Poletto & Sarkar《Linear Scan Register Allocation》——index.md 列出。
  • slides 的图着色讲解 credit Stanford CS143;活跃性框架 credit 6.035 (Amarasinghe)。
  • 系统性教材:《Engineering a Compiler》(index.md 推荐,含寄存器分配/数据流/SSA)。Crafting Interpreters 不涉及本主题。

8. 本讲小结

  • 寄存器分配 = 给干涉图 k-着色(k = 可用寄存器数);干涉 = 活跃区间重叠。
  • 图着色(Chaitin-Briggs):Build → Simplify(移度数<k 的点)→ 必要时标记 Spill → Select(弹栈着色)→ 实际溢出则插溢出代码重跑;用 coalescing 消除 move。质量高但 NP 完全、编译慢,适合 AOT。
  • 线性扫描:按区间 start 排序单趟扫描,维护 active 集合,到期释放寄存器、不够则溢出 end 最晚者;近线性、简单,适合 JIT。
  • 两者都必须遵守寄存器一致性与 ISA 约束(特定寄存器、callee-saved、溢出 load/store)。
  • 选择取决于编译时机:JIT→线性扫描,AOT→图着色。
  • 下一单元(L23–L24):静态分析,把 L21 的活跃性数据流一般化为通用分析框架(格、单调函数、不动点)。