高级 #compiler#registers#graph-coloring

寄存器分配(图着色)

寄存器分配(Register Allocation)决定哪些变量存寄存器、哪些"溢出"到内存——图着色是最经典的算法,把寄存器分配转化为给"干扰图"着色的问题

🪑 变量多,寄存器少——谁”坐”寄存器?

CPU 的寄存器数量有限(x86-64 有 16 个通用寄存器,ARM64 有 31 个)。但一个程序可能有成百上千个变量。

寄存器分配(Register Allocation) 决定:哪些变量放在极快的寄存器中,哪些”溢出”到较慢的栈内存。

🏪 **类比:星巴克的座位”

星巴克有 20 个座位(寄存器),但有 100 个顾客(变量)。

  • 谁”配”坐座位?→ 谁用得更频繁、更迫切
  • 坐不下的人怎么办?→ 打包带走(溢出到内存)
  • 如果坐着的某个人很久没动(长时间不使用)→ 请他把座位让给更需要的人

寄存器分配做的就是”谁坐座位、谁打包带走”的决策。


🔧 图着色算法

图着色是寄存器分配最经典的算法——1979 年由 Chaitin 提出,今天 LLVM、GCC 等主流编译器仍在使用。

第 1 步:构建干扰图

两个变量如果”同时活跃”,就在它们之间画一条边——表示它们不能共享同一个寄存器。

# 干扰图的构建逻辑
# 如果变量 x 和 y 在某个时刻都"活着"——它们冲突

# 代码:a = b + c; d = a * e
# 活跃分析结果:
# b 和 c 在加法前活着 → b 和 c 冲突
# a 在加法后、乘法前活着 → a 与 b、c 不冲突(b、c 已经用完了)
# d 在乘法后活着 → d 与 a 冲突

interference_graph = {
    "a": {"d", "e"},
    "b": {"c", "e"},
    "c": {"b"},
    "d": {"a", "e"},
    "e": {"a", "b", "d"},
}

第 2 步:着色

把颜色分配给每个节点——相邻节点不能同色。颜色数量 = 可用寄存器数量。

干扰图:
   b ─── c
   │     │
   e ─── a ─── d

可用颜色:R, G, B(3 个寄存器)
着色方案:
  a → R    b → G    c → B    d → G    e → B

检查:相邻的 a(R) 和 e(B) 颜色不同 ✅
     相邻的 b(G) 和 e(B) 颜色不同 ✅
     ...

第 3 步:溢出

如果颜色不够用——某些变量必须”溢出”到栈内存。

如果只有 2 个可用寄存器——无法用 2 种颜色给上图着色
→ 选择一个变量(通常选使用最少的)溢出到内存
→ 从干扰图中去掉它 → 重新着色

📊 寄存器 vs 内存的速度差异

存储位置访问延迟相对速度
寄存器~0.3 ns⚡ 最快
L1 缓存~1 ns次快
L2 缓存~4 ns中等
主存(RAM)~50 ns🐢 慢 ~150 倍

一次”溢出”到内存意味着每次访问该变量要等 50+ ns——这就是为什么好的寄存器分配对性能至关重要。


📝 小结

概念一句话
寄存器分配决定哪个变量放寄存器——“座位分配”问题
干扰图两个变量同时活跃→冲突→不能共享寄存器
图着色给干扰图的节点分配颜色(寄存器),相邻节点不能同色
溢出(Spill)寄存器不够用时——变量存到栈内存
重要性寄存器比内存快 ~150 倍——好的分配可以大幅提升性能

为什么先学这个? 分配好寄存器后,进入基本优化(常量折叠、死代码消除)