--- title: "第五部分: Coalescing 与 Iterated Register Coalescing" slug: "coalescing" lang: zh-Hans series: register-allocation weight: 60 created: 2026-08-14 updated: 2026-09-09 license: CC-BY-SA-4.0 --- 第四部分暂时把 interference graph 当成了一张只包含硬约束的图: 两个节点之间有边, 就必须分配不同颜色. 真实机器 IR 中还大量存在另一类关系, 即 COPY 或 move. 对于一条 `b = COPY a`, allocator 希望 $a$ 和 $b$ 最终获得同一个物理寄存器, 这样 COPY 就可以消失. 这使寄存器分配同时面对两种方向相反的约束: interference 希望两个节点分开, move 希望两个节点靠拢. COPY 的来源很多. SSA destruction 会把 phi 转换成边上的 copy, calling convention 会在参数寄存器和普通虚拟寄存器之间产生 copy, instruction selection 和 two-address lowering 也会引入 move. Live range splitting 本身还会制造新的 copy, 用于连接同一个逻辑值的不同 fragments. 因此 allocator 如果完全忽略 move, 最终机器代码里往往会保留大量本可消除的寄存器间复制. 让 COPY 两端获得同一个颜色的操作称为 register coalescing. 问题在于, coalescing 会合并两个 live ranges, 也会合并它们各自的 interference relation. 一次看起来很有收益的 move elimination, 可能让 interference graph 变得更难着色. Iterated Register Coalescing, 简称 IRC, 就是在 simplify, coalesce, freeze 和 spill 之间反复切换, 尽量消除 move, 同时控制 coalescing 对 colorability 的破坏. ## 1 为什么不能看到 COPY 就直接合并 考虑: ```text v = COPY u ``` 如果 $u$ 和 $v$ 没有 interference edge, 最直观的想法就是把两个节点合成一个节点 $uv$. 合并之后所有对 $u$ 或 $v$ 的引用都视为对同一个节点的引用, 如果最终 $uv\mapsto R_1$, 原来的 COPY 就会变成 `R1 = COPY R1`, 随后删除. 但 "没有 interference edge" 只说明 $u$ 和 $v$ 的生命周期允许共享寄存器, 并不能保证整张图仍然存在一个让它们共享颜色的 $K$-coloring. 假设 $K=3$, 有三个节点 $a$, $b$, $c$ 两两冲突, 因而形成一个三角形. 此外 $u$ 和 $a,b$ 冲突, $v$ 和 $b,c$ 冲突, $u$ 与 $v$ 之间存在 COPY, 但没有 interference edge: ```text u v / \ / \ a---b-------? c \___________/ a, b, c form a triangle u interferes with a, b v interferes with b, c u <---- COPY ----> v ``` 更准确地列出边就是: ```text a -- b b -- c a -- c u -- a u -- b v -- b v -- c ``` 三角形 $a,b,c$ 在三个寄存器下必须使用三种不同颜色. 假设 $a\mapsto R_0$, $b\mapsto R_1$, $c\mapsto R_2$. 那么 $u$ 与 $a,b$ 冲突, 所以只能使用 $R_2$; $v$ 与 $b,c$ 冲突, 所以只能使用 $R_0$. 原图完全可以 3-color, 只是 $u$ 和 $v$ 必须使用不同颜色. 如果强行把 $u$ 和 $v$ 合并成 $w$, 那么 $w$ 会同时与 $a$, $b$, $c$ 冲突. 合并后的 $a,b,c,w$ 构成 $K_4$, 需要四种颜色. 原来能够在三个寄存器中完成分配的图, 经过这次 coalescing 后就无法再 3-color. 所以 coalescing 需要一个保守条件. allocator 希望确认这次合并不会明显破坏后续 coloring 的机会, 然后才真正把两个节点收缩成一个. 这种策略通常称为 conservative coalescing. ## 2 Briggs criterion 与 George criterion Briggs criterion 从合并之后节点的 high-degree neighbors 数量出发. 假设准备合并 $u$ 和 $v$, 目标机器有 $K$ 个颜色. 先取两个节点当前邻居集合的并集 $Adj(u)\cup Adj(v)$, 再观察其中有多少节点满足 $degree(t)\ge K$. 如果这样的 high-degree neighbors 少于 $K$ 个, Briggs 认为这次合并足够保守. 也就是说, Briggs 检查的是 $Adj(u)\cup Adj(v)$ 中 significant nodes 的数量, 其中 significant 通常指 $degree\ge K$ 的节点. 条件可以在正文里写成: 如果 $\left|{t\in Adj(u)\cup Adj(v)\mid degree(t)\ge K}\right| v2 v2 -> v1 ``` 那么: ```text GetAlias(v3) = v1 ``` 最终 coloring 完成以后, coalesced nodes 直接继承其代表节点的颜色. 如果 $v$ 被合并到 $u$, 最后有 $color(u)=R_2$, 那么自然得到 $color(v)=R_2$. Coalescing 还会改变 degree. 当 $v$ 的边转移给 $u$ 后, 某些邻居可能新增与 $u$ 的 interference, 某些边则已经存在, 不需要重复计算. 合并后的 $u$ 也可能从 low-degree 变成 high-degree, 因而从一个适合 freeze 的节点转移到 spill candidate 集合. 这说明 coalescing 和 simplify 不能各自独立运行一次, 两者需要反复交替. ## 4 为什么需要 Freeze 假设一个节点 $v$ 满足 $degree(v)+ | v Freeze | v SelectSpill | +------> Simplify main loop ends | v AssignColors | v Rewrite if spilled ``` 实际执行并不会严格按图中的固定环路逐项走一遍. 每次循环都会根据当前 worklist 是否为空选择能够推进的操作, 图和 move 状态随之变化. ## 11 Coalescing 的收益和代价 Coalescing 最直接的收益是减少 register-to-register move. 对 CPU 来说, 某些 move 可能在 rename 阶段成本很低, 某些甚至可以被硬件消除, 但它们仍然可能占用 decode/issue bandwidth, 增加 code size, 并影响调度. 对一些目标机器或特殊 register classes 来说, move 的成本还会更高. 另一方面, coalescing 会延长或合并 live ranges. 原本两个不同时期占用寄存器的值一旦被视为一个更大的 allocation object, 其邻居集合可能增大, register pressure 也可能在某些区域更难处理. 所以 "消除更多 COPY" 和 "避免更多 spill" 并不总是同一个方向. 这也是 conservative coalescing 的基本取舍. Aggressive coalescing 更愿意删除 COPY, 可能付出更多 spill 风险; conservative coalescing 会保留一些 move, 换取更稳定的 colorability. IRC 进一步让这个选择随着 simplify 动态变化, 第一次不安全的 move 可以进入 `activeMoves`, 等 degree 降低后重新尝试. 现代工业 allocator 未必直接实现完整的 textbook IRC 状态机, 但这里形成的几个观念会一直保留下去: COPY 可以形成 register preference, coalescing 会改变 live range 和 interference, 合并需要考虑 pressure, 某些 move 可以推迟处理, 也可以在必要时放弃. 后面看 LLVM RegisterCoalescer, LLVM Greedy 的 register hints, 以及 GCC IRA 的 copy cost 时, 都能看到这些思想的延续. 下一部分进入 Linear Scan Register Allocation. 到那里我们会暂时放下显式 interference graph, 改用 live interval 的线性顺序处理寄存器竞争. 这样可以直接比较两种经典视角: graph coloring 主要保存 "谁和谁冲突", linear scan 则主要保存 "每个值在什么时候存活".