mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1678 字
5 分钟
寄存器分配:图着色与线性扫描
2026-01-12

寄存器分配是编译器后端最关键的问题之一——IR 中有无限多的虚拟寄存器,但目标机器只有有限的物理寄存器(x86-64 只有 16 个通用寄存器)。如何将虚拟寄存器映射到物理寄存器?当物理寄存器不够时怎么办?

寄存器分配是这一章的主角——图着色算法如何找到最优分配?线性扫描如何在编译速度和分配质量之间取得平衡?溢出处理如何决定哪些变量被存入内存?

一、寄存器分配的问题定义#

1.1 从虚拟寄存器到物理寄存器#

flowchart LR subgraph 虚拟寄存器["无限虚拟寄存器"] V1["%v1"] V2["%v2"] V3["%v3"] V4["%v4"] V5["%v5"] VDOT["..."] end subgraph 物理寄存器["有限物理寄存器 (x86-64)"] R1["RAX"] R2["RBX"] R3["RCX"] R4["RDX"] R5["RSI"] R6["RDI"] R7["R8-R15"] end 虚拟寄存器 -->|寄存器分配| 物理寄存器 style 虚拟寄存器 fill:#e3f2fd,stroke:#1565c0 style 物理寄存器 fill:#fff3e0,stroke:#e65100

1.2 问题的形式化定义#

给定:

  • 一组虚拟寄存器 V = {v1, v2, …, vn}
  • 一组物理寄存器 P = {p1, p2, …, pk}(k << n)
  • 干涉关系 I ⊆ V × V:如果 vi 和 vj 同时活跃,则 (vi, vj) ∈ I

求:映射 f: V → P,使得如果 (vi, vj) ∈ I,则 f(vi) ≠ f(vj)

1.3 活跃变量分析#

寄存器分配的前提是活跃变量分析——确定每个程序点哪些变量是”活的”(未来还会被使用):

class LivenessAnalyzer:
"""活跃变量分析"""
def analyze(self, cfg):
"""计算每个程序点的活跃变量集合"""
# 后向数据流分析
live_in = {block: set() for block in cfg.blocks}
live_out = {block: set() for block in cfg.blocks}
changed = True
while changed:
changed = False
for block in reversed(cfg.blocks):
# live_out(B) = ∪ live_in(S) for all successors S
new_out = set()
for succ in block.successors:
new_out |= live_in[succ]
# live_in(B) = use(B) ∪ (live_out(B) - def(B))
use = self._compute_use(block)
defn = self._compute_def(block)
new_in = use | (new_out - defn)
if new_in != live_in[block] or new_out != live_out[block]:
changed = True
live_in[block] = new_in
live_out[block] = new_out
return live_in, live_out

二、干涉图与图着色#

2.1 干涉图的构建#

干涉图的节点是虚拟寄存器,边表示两个寄存器同时活跃(不能分配同一物理寄存器):

class InterferenceGraph:
"""干涉图"""
def __init__(self):
self.nodes = set() # 虚拟寄存器集合
self.edges = set() # 干涉边集合
self.move_edges = set() # 移动指令边(用于合并)
def add_node(self, v):
self.nodes.add(v)
def add_interference(self, v1, v2):
if v1 != v2:
self.edges.add((min(v1, v2), max(v1, v2)))
def add_move(self, v1, v2):
self.move_edges.add((v1, v2))
def neighbors(self, v):
return {u for u in self.nodes if (min(v, u), max(v, u)) in self.edges}
def degree(self, v):
return len(self.neighbors(v))
def build_interference_graph(cfg, live_ranges):
"""从活跃范围构建干涉图"""
ig = InterferenceGraph()
for block in cfg.blocks:
live = set(live_ranges[block].live_out)
for inst in reversed(block.instructions):
# 定义点:变量开始不活跃
if inst.result:
ig.add_node(inst.result)
# 与当前活跃的所有变量干涉
for v in live:
if v != inst.result:
ig.add_interference(inst.result, v)
# 使用点:变量开始活跃
for operand in inst.operands:
if isinstance(operand, str) and operand.startswith('%'):
ig.add_node(operand)
live.add(operand)
# 从活跃集合中移除定义点
if inst.result:
live.discard(inst.result)
# 记录移动指令
if inst.op == 'copy' or inst.op == 'mov':
ig.add_move(inst.result, inst.operands[0])
return ig

2.2 图着色算法#

图着色是 NP 完全问题,编译器使用启发式算法

class GraphColoringAllocator:
"""图着色寄存器分配器"""
def __init__(self, num_registers):
self.K = num_registers # 可用物理寄存器数
def allocate(self, ig):
"""使用简化-选择算法进行图着色"""
stack = []
graph = ig # 工作副本
# 阶段 1:简化(Simplify)
while graph.nodes:
# 选择度数 < K 的节点
low_degree = [v for v in graph.nodes if graph.degree(v) < self.K]
if low_degree:
# 移除度数最低的节点
node = min(low_degree, key=lambda v: graph.degree(v))
graph.nodes.remove(node)
neighbors = graph.neighbors(node)
# 保存邻居信息用于后续恢复
stack.append((node, neighbors, graph.edges))
# 移除相关边
graph.edges = {(a, b) for a, b in graph.edges
if a != node and b != node}
else:
# 所有节点度数 >= K,需要溢出
# 选择溢出代价最低的节点
spill_node = self._select_spill(graph)
graph.nodes.remove(spill_node)
neighbors = graph.neighbors(spill_node)
stack.append((spill_node, neighbors, graph.edges))
graph.edges = {(a, b) for a, b in graph.edges
if a != spill_node and b != spill_node}
# 阶段 2:选择(Select)
coloring = {}
while stack:
node, neighbors, old_edges = stack.pop()
# 找到邻居未使用的颜色
used_colors = {coloring[n] for n in neighbors if n in coloring}
available = set(range(self.K)) - used_colors
if available:
coloring[node] = min(available)
else:
# 溢出(需要重新分配)
coloring[node] = -1 # 标记为溢出
return coloring
def _select_spill(self, graph):
"""选择溢出代价最低的节点"""
# 启发式:选择度数最高但使用频率最低的节点
return max(graph.nodes, key=lambda v: graph.degree(v))

2.3 图着色的流程#

flowchart TB IG["构建干涉图"] --> SIMPLIFY["简化<br/>移除低度数节点"] SIMPLIFY -->{所有节点<br/>度数 < K?} |是| PUSH["压栈"] |否| SPILL["选择溢出节点"] SPILL --> PUSH PUSH -->{栈空?} |否| SIMPLIFY |是| SELECT["选择<br/>弹出节点分配颜色"] SELECT --> DONE["完成"] style IG fill:#e3f2fd,stroke:#1565c0 style SIMPLIFY fill:#fff3e0,stroke:#e65100 style SELECT fill:#e8f5e9,stroke:#2e7d32 style SPILL fill:#fce4ec,stroke:#c62828

三、线性扫描算法#

3.1 基本原理#

线性扫描是一种快速的寄存器分配算法——它不构建干涉图,而是按时间顺序扫描活跃区间:

sequenceDiagram participant SRC as 源码 participant IR as IR 指令 participant LS as 线性扫描 participant REG as 寄存器分配结果 SRC->>IR: 生成三地址码 IR->>LS: 计算活跃区间 Note over LS: 按起始点排序区间<br/>[t1:1→5, t2:2→8, t3:3→4...] LS->>LS: 逐个扫描区间 LS->>REG: 分配/溢出决策 Note over REG: t1→RAX, t2→RBX<br/>t3→溢出(栈偏移+8)
Tip

线性扫描不构建干涉图,时间复杂度 O(n log n),远优于图着色的 O(n²)。JIT 编译器(V8、HotSpot)几乎都采用线性扫描或其变体,因为编译延迟直接影响运行时性能。

class LinearScanAllocator:
"""线性扫描寄存器分配器"""
def __init__(self, num_registers):
self.K = num_registers
def allocate(self, intervals):
"""
intervals: 活跃区间列表,每个区间 (start, end, var_name)
"""
# 按起始点排序
intervals.sort(key=lambda x: x[0])
active = [] # 当前活跃的区间(按结束点排序)
allocation = {} # 变量 → 物理寄存器
free_regs = list(range(self.K)) # 空闲寄存器池
for interval in intervals:
# 过期:移除已结束的活跃区间
active = [a for a in active if a[1] > interval[0]]
for a in active:
if a[1] <= interval[0]:
free_regs.append(allocation[a[2]])
if len(active) == self.K:
# 溢出:溢出结束点最远的区间
spill = max(active, key=lambda x: x[1])
if spill[1] > interval[1]:
# 溢出 spill,将 interval 分配到 spill 的寄存器
allocation[interval[2]] = allocation[spill[2]]
allocation[spill[2]] = -1 # 标记溢出
active.remove(spill)
active.append(interval)
else:
# 溢出当前区间
allocation[interval[2]] = -1
else:
# 分配空闲寄存器
reg = free_regs.pop(0)
allocation[interval[2]] = reg
active.append(interval)
return allocation

3.2 线性扫描的流程#

flowchart TB SORT["按起始点排序<br/>活跃区间"] --> SCAN["扫描每个区间"] SCAN --> EXPIRE["移除过期区间<br/>释放寄存器"] EXPIRE --> HAS_REG{有空闲<br/>寄存器?} HAS_REG |是| ALLOC["分配寄存器"] HAS_REG |否| SPILL["溢出最远区间"] ALLOC --> NEXT{还有区间?} SPILL --> NEXT NEXT |是| SCAN NEXT |否| DONE["完成"] style SORT fill:#e3f2fd,stroke:#1565c0 style ALLOC fill:#e8f5e9,stroke:#2e7d32 style SPILL fill:#fce4ec,stroke:#c62828

3.3 图着色 vs 线性扫描#

特性图着色线性扫描
分配质量
编译速度慢 O(n²)快 O(n log n)
实现复杂度
溢出质量一般
合并优化支持有限
使用场景优化编译 (-O2/-O3)JIT 编译
典型用户GCC, LLVMV8, HotSpot JIT

四、溢出处理#

4.1 溢出的代价#

当物理寄存器不够时,编译器必须将某些变量**溢出(Spill)**到内存(栈帧):

// 无溢出:全部在寄存器中
add rax, rbx ; 寄存器加法,1 周期
// 有溢出:需要 load/store
mov [rsp+8], rax ; 保存 rax 到栈
mov rax, [rsp+16] ; 从栈加载溢出变量
add rax, rbx ; 执行加法
mov rax, [rsp+8] ; 恢复 rax
操作延迟(典型)吞吐量
寄存器加法1 周期4/周期
L1 缓存 load4-5 周期2/周期
L2 缓存 load12-15 周期1/周期
L3 缓存 load30-50 周期0.5/周期
主存 load100-300 周期

4.2 溢出代价估计#

def estimate_spill_cost(var, loop_depth, use_count, def_count):
"""估计溢出一个变量的代价"""
# 循环内的溢出代价更高
loop_weight = 10 ** loop_depth
# 每次 use/def 需要 load/store
cost = (use_count + def_count) * loop_weight
return cost

4.3 溢出策略#

策略说明适用场景
全量溢出变量完全在内存中简单但代价高
部分溢出只在压力大的区域溢出更优但复杂
分割活跃区间将长区间分割为短区间减少干涉
重新物化重新计算而非 load常量/简单计算
Warning

溢出可能导致溢出级联——溢出一个变量释放了一个寄存器,但增加了 load/store 指令,这些指令又需要新的虚拟寄存器,可能导致新的溢出。编译器需要迭代直到收敛。

五、寄存器合并(Coalescing)#

5.1 基本原理#

寄存器合并消除不必要的复制指令——如果两个虚拟寄存器不干涉且由 copy 指令连接,可以将它们分配到同一物理寄存器:

// 优化前
mov rax, rbx ; copy 指令
add rax, rcx
// 合并后(rbx 和 rax 分配同一物理寄存器)
add rbx, rcx ; copy 被消除

5.2 合并算法#

class Coalescer:
"""寄存器合并器"""
def coalesce(self, ig, allocation):
"""合并 copy 相关的虚拟寄存器"""
changed = True
while changed:
changed = False
for v1, v2 in list(ig.move_edges):
# 检查是否可以合并
if not self._interferes(ig, v1, v2):
# 合并 v2 到 v1
self._merge(ig, v1, v2)
allocation[v2] = allocation[v1]
changed = True
return allocation
def _interferes(self, ig, v1, v2):
"""检查两个变量是否干涉"""
return (min(v1, v2), max(v1, v2)) in ig.edges
def _merge(self, ig, v1, v2):
"""合并两个节点"""
# 将 v2 的所有边转移到 v1
for neighbor in ig.neighbors(v2):
ig.add_interference(v1, neighbor)
# 移除 v2
ig.nodes.discard(v2)
ig.edges = {(a, b) for a, b in ig.edges if a != v2 and b != v2}
ig.move_edges.discard((v1, v2))
ig.move_edges.discard((v2, v1))

5.3 合并策略对比#

策略原理优点缺点
激进合并尽可能合并消除最多 copy可能增加溢出
保守合并只合并不增加度数的安全可能错过合并机会
迭代合并合并→着色→再合并平衡编译慢

六、不同架构的寄存器分配#

6.1 不同架构的寄存器资源#

架构通用寄存器数特殊约束
x86 (32位)8寄存器极少,大量约束
x86-6416较好,但指令有固定寄存器要求
ARM (32位)16较好,RISC 设计
AArch6431丰富,几乎无约束
RISC-V31丰富,干净设计
WASM无限(栈机)无寄存器分配问题

6.2 x86 的特殊约束#

x86 有大量指令固定使用特定寄存器的约束:

// 除法指令固定使用 RDX:RAX
idiv rbx ; RAX = RDX:RAX / rbx, RDX = RDX:RAX % rbx
// 系统调用固定使用 RAX(调用号)
mov rax, 1 ; sys_write
syscall
// 移位指令固定使用 CL
shl rax, cl ; 移位量必须在 CL 中

这些约束使得 x86 的寄存器分配比 RISC 架构复杂得多。

七、寄存器分配的实际影响#

7.1 寄存器压力对性能的影响#

// 低寄存器压力:3 个变量,6 个寄存器足够
void low_pressure(int* a, int* b, int* c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i]; // i, a[i], b[i], c[i] → 4 个寄存器
}
}
// 高寄存器压力:8 个变量,6 个寄存器不够
void high_pressure(int* a, int* b, int* c, int* d,
int* e, int* f, int* g, int* h, int n) {
for (int i = 0; i < n; i++) {
h[i] = a[i]*b[i] + c[i]*d[i] + e[i]*f[i] + g[i];
// 需要 a,b,c,d,e,f,g,h,i → 9 个寄存器
}
}

7.2 减少寄存器压力的编程技巧#

技巧说明效果
减少同时活跃的变量分解复杂表达式直接减少压力
使用局部变量限制变量生命周期缩短活跃区间
避免深层嵌套减少同时活跃的变量降低峰值压力
使用 __restrict__消除别名检查减少临时变量

八、动手实践#

8.1 实验一:观察寄存器分配#

cat > reg_alloc.c << 'EOF'
int add(int a, int b) { return a + b; }
int complex(int a, int b, int c, int d, int e, int f) {
return a*b + c*d + e*f;
}
EOF
clang -O0 -S reg_alloc.c -o reg_O0.s
clang -O2 -S reg_alloc.c -o reg_O2.s
# 对比:-O0 大量使用栈,-O2 全部在寄存器中
grep -c "mov.*rsp" reg_O0.s reg_O2.s

8.2 实验二:观察溢出#

cat > spill.c << 'EOF'
// 故意制造高寄存器压力
int spill_test(int a, int b, int c, int d,
int e, int f, int g, int h) {
return a + b + c + d + e + f + g + h;
}
EOF
clang -O2 -S spill.c -o spill.s
# 查看是否有 spill/reload 指令
grep -E "(spill|reload|mov.*rsp)" spill.s

8.3 实验三:LLVM 的寄存器分配#

# 查看 LLVM 的寄存器分配过程
clang -O2 -emit-llvm -S spill.c -o spill.ll
llc -debug spill.ll -o spill.s 2>&1 | grep -i "regalloc\|spill\|assign"

九、本章小结#

上一章中,循环展开和向量化显著提升了热点代码的性能,但所有这些优化都假设虚拟寄存器是无限的。现实是 x86-64 只有 16 个通用寄存器——当虚拟寄存器数量超过物理寄存器时,编译器必须决定哪些值留在寄存器中,哪些溢出到内存。这就是寄存器分配要解决的核心问题。

概念要点
问题定义将无限虚拟寄存器映射到有限物理寄存器
活跃变量分析后向数据流分析,确定变量活跃区间
干涉图节点=虚拟寄存器,边=同时活跃
图着色简化-选择算法,NP 完全,启发式求解
线性扫描按时间顺序扫描,O(n log n),JIT 首选
溢出物理寄存器不够时存入内存,代价高
合并消除 copy 指令,激进 vs 保守策略
架构约束x86 约束多,RISC 约束少

以上就是寄存器分配的核心内容。下一章进入 指令选择与调度,看看 IR 如何映射到目标机器的指令集。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

寄存器分配:图着色与线性扫描
https://blog.souloss.com/posts/compiler/register-allocation/
作者
Souloss
发布于
2026-01-12
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时