寄存器分配是LLVM后端将虚拟寄存器映射到有限物理寄存器的核心阶段,发生于指令选择之后、后RA调度之前;默认采用GreedyRegisterAllocator(LLVM 10起),依据活跃区间长度与使用频率贪心分配,并在寄存器不足时执行spill/reload。

寄存器分配(Register Allocation)是核心环节
虚拟寄存器到物理寄存器的映射不是自动发生的,它发生在 LLVM 后端的 Register Allocation 阶段,紧接在指令选择(SelectionDAGISel 或 GlobalISel)之后、后 RA 调度之前。这个阶段的目标很明确:把 IR 中无限多的 %vreg123 映射到目标平台真实有限的物理寄存器(比如 x86 的 %rax、%xmm0),或决定哪些必须 spill 到栈上。
LLVM 默认用的是 GreedyRegisterAllocator
从 LLVM 10 开始,Greedy 成为默认的寄存器分配器(取代了旧的 LinearScan)。它不基于图着色,而是按“活跃区间长度 + 使用频率”贪心地分配,再用 spill 和 reload 填补缺口。它的行为受几个关键因素影响:
-
TargetRegisterInfo提供物理寄存器集、调用约定、保留寄存器(如 x86 的%rsp)、寄存器类(GR64,FPR128)等元信息 -
LiveIntervals分析每个虚拟寄存器的活跃范围,这是分配决策的基础 - 函数内联、循环优化等中端 pass 会显著改变活跃区间分布,间接影响 spill 次数
- 使用
-regalloc=fast会跳过Greedy,改用更轻量但保守的分配器,适合 debug 构建
常见错误:spill/reload 过多或分配失败
你看到大量 mov %rax, -8(%rbp) 和 mov -8(%rbp), %rax,基本就是寄存器不够用了。典型诱因包括:
- 目标平台物理寄存器极少(如 RISC-V 32 位嵌入式变种只有 15 个通用寄存器),而 IR 里有大量并行计算
- 未正确标记
call指令的 clobber list,导致分配器误以为某些寄存器全程不可用 - 自定义后端中
TargetRegisterInfo::getReservedRegs()返回了过多寄存器(比如把所有 callee-saved 都设为 reserved) - 使用
GlobalISel时未提供完整的LegalizerInfo,导致某些操作被降级成多个指令,虚寄数量暴增
调试寄存器分配过程的关键命令
想确认某次编译中到底发生了什么,最直接的方式是让 LLVM 输出中间状态:
- 加
-print-machineinstrs查看 MachineInstr 阶段的寄存器名(仍是%vreg) - 加
-debug-only=regalloc输出Greedy的每一步决策,包括哪个 vreg 分配给了哪个 physreg、为什么 spill - 加
-run-pass=regalloc强制只运行寄存器分配阶段,配合-o /dev/null快速验证配置 - 生成
.ll→.bc→.s流程中,在llc -march=x86-64 -debug-pass=Structure下能看到各 pass 执行顺序
真正难处理的从来不是“怎么触发分配”,而是理解为什么某个 vreg 被 spill —— 它往往暴露的是前端生成 IR 的冗余,或是后端对调用约定/寄存器类的描述偏差。盯着 LiveInterval 和 clobber 列表比调参数更有效。

















