强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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, %badd
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
RegAllocPBQPPBQP 算法-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 IRSelectionDAGSelectionDAGBuilder
合法化SelectionDAG合法 DAGLegalizeDAG
指令选择DAGMachineInstrSelectionDAGISel
Pre-RA 调度MachineInstr有序指令ScheduleDAG
寄存器分配虚拟寄存器物理寄存器RegAllocGreedy
Post-RA 调度分配后指令最终顺序PostRAScheduler
Peephole机器指令优化后指令PeepholeOptimizer
发射MachineInstrMCInstAsmPrinter

扩展阅读

  1. LLVM Code Generator — 代码生成器文档
  2. SelectionDAG — SelectionDAG 文档
  3. Register Allocation — 寄存器分配
  4. Machine IR — MIR 格式参考

下一章: 第 10 章:JIT 编译 — 学习 LLVM 的即时编译框架 ORC JIT 和 MCJIT。