LLVM 开发指南 / 第 9 章:代码生成
第 9 章:代码生成
“代码生成是将高级 IR 降低到机器指令的艺术。”
9.1 代码生成流程概览
LLVM 后端将 LLVM IR 转换为目标机器码,经历多个阶段:
LLVM IR
│
▼
┌──────────────────────┐
│ 1. IR → SelectionDAG │ SelectionDAGBuilder
│ 将 IR 转为 DAG │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 2. DAG 合法化 │ SelectionDAGLegalize
│ 类型和操作合法化 │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 3. 指令选择 │ SelectionDAGISel
│ DAG → MachineInstr │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 4. 调度 (Pre-RA) │ ScheduleDAGRRList
│ 指令重排序 │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 5. 寄存器分配 │ RegAllocGreedy
│ 虚拟→物理寄存器 │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 6. 调度 (Post-RA) │ PostRAScheduler
│ 最终指令调度 │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 7. Peephole 优化 │ PeepholeOptimizer
│ 窥孔优化 │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ 8. 指令发射 │ AsmPrinter
│ → MCInst → 机器码 │
└──────────┬───────────┘
▼
目标文件 (.o)
9.2 SelectionDAG
SelectionDAG(有向无环图)是指令选择阶段的核心数据结构。
9.2.1 DAG 构建
// 使用 llc 查看 SelectionDAG
// clang -O2 -c test.c 分步查看
llc -O2 -view-dags test.bc # 用 GraphViz 查看
llc -O2 -print-after-isel test.bc # 打印指令选择后的 MIR
llc -O2 -stop-after=instruction-select test.bc -o test.mir
9.2.2 DAG 节点类型
| DAG 节点 | 说明 | 示例 |
|---|---|---|
ISD::ADD | 整数加法 | %r = add %a, %b → add |
ISD::LOAD | 内存加载 | load |
ISD::STORE | 内存存储 | store |
ISD::BRCOND | 条件分支 | brcc |
ISD::CALL | 函数调用 | callseq_start/end |
ISD::SETCC | 比较 | setcc |
ISD::SELECT | 条件选择 | select |
9.2.3 查看 DAG 各阶段
# 查看 DAG 构建后
llc -view-dag-combine1-dags test.bc # DAG 合并前
llc -view-legalize-types-dags test.bc # 类型合法化后
llc -view-dag-combine2-dags test.bc # 第二次合并后
llc -view-isel-dags test.bc # 指令选择前
llc -view-sched-dags test.bc # 调度后
# 打印到终端(不使用 GraphViz)
llc -debug-only=isel test.bc 2>&1
9.3 指令选择 (Instruction Selection)
指令选择将通用 DAG 节点映射到目标特定的指令。
9.3.1 TableGen 指令定义
// X86InstrArithmetic.td (简化示例)
// 定义 ADD 指令
// 指令模式(pattern)
def ADD32rr : I<0x01, MRMDrrReg, (outs GR32:$dst),
(ins GR32:$src1, GR32:$src2),
"add{l}\t{$src2, $dst|$dst, $src2}",
[(set GR32:$dst, (add GR32:$src1, GR32:$src2))]>;
def ADD32ri : I<0x81, MRMDr0, (outs GR32:$dst),
(ins GR32:$src1, i32imm:$src2),
"add{l}\t{$src2, $dst|$dst, $src2}",
[(set GR32:$dst, (add GR32:$src1, imm:$src2))]>;
// 指令模式解读:
// (set GR32:$dst, (add GR32:$src1, GR32:$src2))
// 匹配: DAG 中的 add 节点,两个 32 位通用寄存器操作数
// 结果: 生成 ADD32rr 指令,输出到物理寄存器
9.3.2 指令选择过程
输入 DAG:
t1: i32 = add t2, t3
t2: i32 = load ...
t3: i32 = load ...
指令选择后(X86):
%vreg1: GR32 = MOV32rm ...
%vreg2: GR32 = MOV32rm ...
%vreg3: GR32 = ADD32rr %vreg1, %vreg2
9.4 MachineIR (MIR)
MachineIR 是指令选择之后、汇编发射之前的中间表示。
9.4.1 MIR 格式
# 生成 MIR
llc -stop-after=instruction-select test.bc -o test.mir
# 查看 MIR
cat test.mir
MIR 示例:
--- |
define i32 @add(i32 %a, i32 %b) {
entry:
%0 = add i32 %a, %b
ret i32 %0
}
...
---
name: add
tracksRegLiveness: true
body: |
bb.0.entry:
liveins: $edi, $esi
%0:gr32 = COPY $edi
%1:gr32 = COPY $esi
%2:gr32 = ADD32rr %0, killed %1, implicit-def $eflags
$eax = COPY %2
RET64 implicit $eax
...
9.4.2 MIR 的关键概念
| 概念 | 说明 |
|---|---|
MachineFunction | 对应 LLVM IR 的 Function |
MachineBasicBlock | 对应 LLVM IR 的 BasicBlock |
MachineInstr | 机器指令 |
MachineOperand | 指令操作数(寄存器、立即数等) |
VirtualRegister | 虚拟寄存器(%0, %1, …) |
PhysicalRegister | 物理寄存器($eax, $rdi, …) |
9.5 寄存器分配 (Register Allocation)
9.5.1 寄存器分配概述
指令选择后使用虚拟寄存器,数量不受限制。寄存器分配将它们映射到有限的物理寄存器。
分配前: 分配后:
%vreg1 = ... $eax = ...
%vreg2 = ... $ecx = ...
%vreg3 = add %vreg1, %vreg2 $eax = add $eax, $ecx
%vreg4 = ... $edx = ...
... ...
(无限虚拟寄存器) (有限物理寄存器)
9.5.2 LLVM 的寄存器分配器
| 分配器 | 说明 | 选项 |
|---|---|---|
RegAllocGreedy | 贪心算法(默认) | -regalloc=greedy |
RegAllocFast | 快速分配 | -regalloc=fast |
RegAllocBasic | 基础算法 | -regalloc=basic |
RegAllocPBQP | PBQP 算法 | -regalloc=pbqp |
# 查看寄存器分配过程
llc -regalloc=greedy -debug-only=regalloc test.bc 2>&1
# 使用快速寄存器分配器(编译更快,质量较差)
llc -regalloc=fast test.bc -o test.s
# 查看溢出/恢复
llc -print-after=greedy test.bc -o /dev/null 2>&1
9.5.3 寄存器溢出 (Spill)
当物理寄存器不够时,必须将变量溢出(spill)到栈上:
; 溢出前 — 需要 4 个寄存器
mov eax, [rdi]
mov ecx, [rsi]
add eax, ecx
mov edx, [rdx]
imul eax, edx
ret
; 溢出后 — 只有 3 个寄存器可用
mov eax, [rdi]
mov ecx, [rsi]
add eax, ecx
mov [rsp-4], eax ; 溢出到栈
mov edx, [rdx]
mov eax, [rsp-4] ; 从栈恢复
imul eax, edx
ret
9.6 指令调度 (Instruction Scheduling)
9.6.1 调度的目标
指令调度重排指令顺序以最大化指令级并行(ILP):
调度前 (低 ILP): 调度后 (高 ILP):
load rax, [rdi] load rax, [rdi]
add rax, 1 load rbx, [rsi] ← 与 load 并行
load rbx, [rsi] add rax, 1
add rbx, 2 add rbx, 2
mul rax, rbx mul rax, rbx
9.6.2 两阶段调度
| 阶段 | 时机 | 目标 |
|---|---|---|
| Pre-RA Scheduling | 寄存器分配前 | 减少寄存器压力 |
| Post-RA Scheduling | 寄存器分配后 | 最大化 ILP |
9.7 目标特定低层 (TargetLowering)
9.7.1 操作合法化
某些 IR 操作在特定目标上不合法,需要转换:
// X86TargetLowering::LowerOperation 示例
// 64 位除法在 32 位模式下不合法
// 需要替换为库函数调用或扩展操作
// i64 div → __divdi3() 调用
// 向量类型可能需要拆分
// <16 x i8> add → 2 个 <8 x i8> add(在不支持 128 位 SIMD 的目标上)
9.7.2 类型合法化
类型合法化策略:
1. Promotion: i8 → i32(提升到合法类型)
2. Expansion: i128 → 2 x i64(拆分为多个合法类型)
3. Scalarization: <2 x i32> → 2 x i32(向量标量化)
4. Widen: <3 x i32> → <4 x i32>(向量加宽)
9.8 Peephole 优化
后端最后阶段的窥孔优化:
; 优化前
mov eax, edi
add eax, 0 ; 冗余加零
; 优化后 — 消除冗余指令
mov eax, edi
; 优化前
mov eax, 0
; 优化后 — 使用 xor
xor eax, eax ; 更小、更快
; 优化前
imul eax, 8
; 优化后 — 移位更快
shl eax, 3
; LEA 优化
lea eax, [rdi + rdi*4] ; eax = rdi * 5
9.9 MC 层发射
9.9.1 AsmPrinter
// AsmPrinter 将 MachineInstr 转换为 MCInst
// 然后通过 MCStreamer 输出
// 汇编输出
MCInst Inst;
Inst.setOpcode(X86::ADD32rr);
Inst.addOperand(MCOperand::createReg(X86::EAX));
Inst.addOperand(MCOperand::createReg(X86::ECX));
// 通过 MCInstPrinter 输出为汇编文本
// → "addl %ecx, %eax"
// 通过 MCCodeEmitter 输出为机器码
// → [01, 0xC8] (x86 ADD 指令编码)
9.9.2 完整的后端输出
# 查看完整的代码生成过程
llc -print-after-all test.bc -o test.s 2>&1 | less
# 仅查看寄存器分配后的 MIR
llc -stop-after=regalloc test.bc -o test-after-ra.mir
# 仅查看最终 MIR
llc -stop-after=finalize-isel test.bc -o test-final.mir
# 输出汇编
llc test.bc -o test.s
# 输出目标文件
llc -filetype=obj test.bc -o test.o
# 反汇编
llvm-objdump -d test.o
9.10 本章小结
| 阶段 | 输入 | 输出 | 关键组件 |
|---|---|---|---|
| DAG 构建 | LLVM IR | SelectionDAG | SelectionDAGBuilder |
| 合法化 | SelectionDAG | 合法 DAG | LegalizeDAG |
| 指令选择 | DAG | MachineInstr | SelectionDAGISel |
| Pre-RA 调度 | MachineInstr | 有序指令 | ScheduleDAG |
| 寄存器分配 | 虚拟寄存器 | 物理寄存器 | RegAllocGreedy |
| Post-RA 调度 | 分配后指令 | 最终顺序 | PostRAScheduler |
| Peephole | 机器指令 | 优化后指令 | PeepholeOptimizer |
| 发射 | MachineInstr | MCInst | AsmPrinter |
扩展阅读
- LLVM Code Generator — 代码生成器文档
- SelectionDAG — SelectionDAG 文档
- Register Allocation — 寄存器分配
- Machine IR — MIR 格式参考
下一章: 第 10 章:JIT 编译 — 学习 LLVM 的即时编译框架 ORC JIT 和 MCJIT。