这是我啃完一个 LLVM 后端 Lowering 技术讲座后整理的笔记,边看视频边截图,把 Lowering 的全链路串了一遍。如果你也在写 LLVM Backend,或者被 Legalize、DAGCombine、Custom Lowering 这些概念绕晕过,这篇文章应该能帮你把思路捋顺。
一、先说说为什么 Lowering 容易让人崩溃
我刚接触 LLVM Backend 的时候,最困惑的不是怎么写 .td 文件,而是 SelectionDAG 这层到底在干什么。
IR 不就是低级中间表示了么?为什么还要经过 SelectionDAG,为什么还要 Legalize,为什么 DAGCombine 要跑好几遍?这些问题在官方文档里散布在各处,阅读门槛不低。
这个视频讲座正好把 Lowering 的完整流程串讲了一遍,从 SelectionDAGBuilder 到 TargetLowering,从 LegalizeTypes 到 DAGCombine,中间还拿 SDIVREM 的 Lowering 做了案例剖析。下面我会结合视频内容和补充资料,一步步拆解。
二、Lowering 的整体流程:一张图就够了
视频里第一张关键幻灯片就是这个 Lowering 的全景流程图:

整个 Lowering 流程可以拆成这几个环节:
LLVM IR
│
▼
SelectionDAGBuilder ← 把 IR 指令映射成 DAG 节点
│
▼
DAG Combine 1 ← 第一轮优化(类型合法化之前)
│
▼
Type Legalization ← 确保所有类型都是目标平台支持的
│
▼
Operation Legalization ← 确保所有操作都是目标平台支持的
│
▼
DAG Combine 2 ← 第二轮优化(合法化之后)
│
▼
Target Lowering ← 平台特定的 Lowering 回调
│
▼
Instruction Selection ← 选指令,生成 MachineInstr
这里最容易搞混的一对概念:LegalizeTypes 和 LegalizeOperations。
它们是两个独立的 Pass。类型的合法化在先,操作的合法化在后。LLVM 设计这个顺序有它的道理------只有类型合法了,操作合法化才有稳定的输入去判断该不该 Expand、该不该 Promote。
三、SelectionDAGBuilder:IR 指令的 DAG 化
视频花了大量时间演示 SelectionDAGBuilder 的工作方式。这块代码在 SelectionDAGBuilder.cpp 里,核心入口是 visit 函数------一条 IR 指令进来,被派发到对应的 visitXXX 方法。

举个例子,load 指令的 DAG 化逻辑:
cpp
void SelectionDAGBuilder::visitLoad(const LoadInst &I) {
// 1. 计算地址 → 生成 ISD::TokenFactor + ISD::LOAD 节点
// 2. 处理对齐、volatile、原子序等属性
// 3. 构建 chain 和 value 两个输出边
}
这里面有个关键概念:chain(链) 。DAG 不只是数据依赖图,它还包含控制依赖边(chain)和粘合边(glue)。load 的 chain 输出连接到下一条可能有副作用的指令,确保内存访问顺序不会被乱序。
不理解 chain 的话,后面讲 Return Lowering 和 Call Lowering 会非常痛苦------那里面的 chain 和 glue 绕来绕去,看代码之前先把概念搞清楚会省很多时间。
四、指令选择:四种架构风格对比
视频里有一张非常重要的对比表,把四种指令选择方式放在一起比较:

| 方式 | 入口 | 核心思路 |
|---|---|---|
| 传统 DAG-to-DAG | Select() |
模式匹配 + 手动选择,每个操作码硬编码映射 |
| RISC-V SelLowerToDAG | RISCVISelLowering |
大量使用 setOperationAction(Custom) + LowerOperation |
| DAGCombiner | DAGCombiner::Run() |
图重写引擎,Worklist 驱动,反复化简直到不动点 |
| SelectionDAGBuilder | visit() |
IR 到 DAG 的第一道转换,构建初始 DAG |
四种方式不是互斥的------实际的后端会同时用到它们。SelectionDAGBuilder 生成初始 DAG,DAGCombiner 做化简,TargetLowering 在合法化阶段介入,最后的 DAG-to-DAG Select() 把 DAG 节点映射成 MachineSDNode。
五、Legalize:类型合法化和操作合法化的区别
这是整个 Lowering 流程中最容易出错的地方。我结合视频内容和补充资料整理了一个对照表。
5.1 LegalizeTypes(类型合法化)
目标:把所有不合法的 ValueType 变成目标支持的合法类型。
它的几个核心动作:
| 动作 | 含义 | 例子 |
|---|---|---|
| Promote | 小类型 → 大类型 | i1 / i8 提升到 i32 |
| Expand | 大类型 → 拆成小类型 | i64 拆成 2 个 i32 |
| ScalarizeVector | 向量 → 标量 | v2i32 拆成 2 个 i32 标量操作 |
关注点:只关心类型,不关心操作本身是否合法。
5.2 LegalizeOperations(操作合法化)
目标:类型合法之后,把不支持的 DAG 节点变成目标支持的操作。
| 动作 | 含义 | 触发方式 |
|---|---|---|
| Expand | 操作拆成更基础的多个操作 | setOperationAction(ISD::XXX, VT, Expand) |
| Promote | 操作提升位宽后再算 | setOperationAction(ISD::XXX, VT, Promote) |
| Custom | 后端自己处理 | setOperationAction(ISD::XXX, VT, Custom) |
| LibCall | 生成库函数调用 | 用于浮点运算等软实现 |
5.3 一句话区分
LegalizeTypes = "你的寄存器能不能装下这个类型"
LegalizeOperations = "你的 ALU 能不能算这个操作"
Custom 是最灵活的------你可以完全控制 Lowering 逻辑。视频里的 SDIVREM 例子就是通过 Custom 来实现的。Expand 和 Promote 是 LLVM 框架自动帮你做的,你只需要在 TargetLowering 的构造函数里声明 setOperationAction 即可。
六、DAG Combine:图重写引擎的迭代收敛
视频里的 DAGCombine 部分讲得比较简要,但这块在实际调优中极其重要。我补充一下它的内核。
DAGCombine 本质上是一个 Worklist 驱动的图重写引擎。伪代码大概长这样:
Worklist.insert(所有初始节点)
while Worklist 非空:
N = Worklist.pop()
if tryCombine(N) 成功:
// DAG 被修改了
Worklist.insert(N)
Worklist.insert(N 的所有使用者)
Worklist.insert(N 的所有操作数)

这里面有三个关键细节:
1. 为什么要迭代?
单次遍历不够。比如第一次 combine 把 (add (mul x, 2), 0) 化简成 (mul x, 2),第二次才能把 (mul x, 2) 化简成 (shl x, 1)。每次都可能暴露新的优化机会。
2. 怎么保证收敛?
- 所有变换是语义等价或优化的,不会"越变越大"
- 变换是幂等的------
add(x, 0) → x第二次不会再触发 - 节点一旦变成 final form,不再进 Worklist
3. 实际怎么用?
你的后端通过 setTargetDAGCombine(ISD::XXX) 声明想参与的节点类型,然后实现 PerformDAGCombine 回调:
cpp
void MyTargetLowering::PerformDAGCombine(SDNode *N, DAGCombinerInfo &DCI) const {
switch (N->getOpcode()) {
case ISD::ADD:
// 自定义合并逻辑:比如 add(x, 0) → x
if (isNullConstant(N->getOperand(1)))
DCI.CombineTo(N, N->getOperand(0));
break;
}
}
七、实战案例:SDIVREM 的 Custom Lowering
视频花了后半段大量时间手把手演示了 SDIVREM 的 Lowering 过程,这是理解 Custom Lowering 的绝佳案例。
7.1 问题背景
SDIVREM 是一条 LLVM IR 指令,同时产生商和余数。但很多 RISC 架构没有这样的一条硬件指令。视频里假设目标平台 Cpu0 只有 DivRemU(无符号除法+取余),没有带符号版本。

7.2 Lowering 策略
在 TargetLowering 构造函数里声明:
cpp
setOperationAction(ISD::SDIVREM, MVT::i32, Custom);
然后在 LowerOperation 里实现分解逻辑:
SDIVREM (输入 A, B)
│
├─ 提取符号位:sign_A = A >> 31, sign_B = B >> 31
├─ 取绝对值:abs_A = (A ^ sign_A) - sign_A
├─ 无符号除法:DivRem = DivRemU(abs_A, abs_B)
├─ 恢复符号:quo = (quo_unsigned ^ xor_sign) - xor_sign
└─ 构建结果:输出 {quo, rem}
这一步一步拆开来的过程,视频里每一帧都有红色箭头标注,哪段代码对应 DAG 的哪个节点一目了然。
7.3 完整数据流
把整个过程展开成 DAG 节点后看起来大概这样:

每个箭头对应一个 DAG 边(数据依赖或 chain/glue 依赖)。重点看 glue 边------它串联起 CopyFromReg → DivRemU → CopyToReg,保证这些节点的 执行顺序 不会被 DAG 调度器重新排列。
这里有个很容易踩的坑:如果漏了 glue 边,DAG 调度器可能会把除法操作的输入读取和输出写回重排,导致寄存器内容错乱。
八、Return Lowering:chain 和 glue 的经典战场
视频在中段专门讲了一段 Return 值的 Lowering。这个过程特别能体现 chain 和 glue 的价值。

一个函数返回结构体的 DAG 大致长这样:
Load (返回值从某地址加载)
│ chain
▼
CopyToReg (把值写入物理寄存器 $v0)
│ glue
▼
CopyToReg (把第二个返回值写入 $v1)
│ glue
▼
Cpu0ISD::Ret (硬件返回指令)
chain 保证 Load 一定在 CopyToReg 之前执行。glue 则把多个 CopyToReg 和最后的 Ret 粘在一起------必须按这个顺序发射。
视频里用红色箭头一层层标注了这个依赖关系,从 ResultVals[i] 的代码到 SDAG 图的 OutVals 节点,再到最终的 Cpu0ISD::Ret。如果你对 chain/glue 的理解还停留在"知道有这东西但没用过"的阶段,这段值得逐帧细看。
九、自定义 Lowering 的完整配方
结合视频内容和补充资料,我整理了一个实战 checklist:
9.1 声明 target-specific SDNode
在 .td 文件里定义你自己的节点:
tablegen
def Cpu0ISD_DivRem : SDNode<"Cpu0ISD::DivRem", SDT_Cpu0DivRem>;
9.2 设置 Action
在 TargetLowering 构造函数里:
cpp
setOperationAction(ISD::SDIVREM, MVT::i32, Custom);
9.3 实现 LowerOperation
cpp
SDValue Cpu0TargetLowering::LowerOperation(SDValue Op, SelectionDAG &DAG) const {
switch (Op.getOpcode()) {
case ISD::SDIVREM:
return LowerSDIVREM(Op, DAG);
// ...
}
return SDValue();
}
9.4 实现 LowerSDIVREM
在 LowerSDIVREM 里构建完整的 DAG 替换链:符号提取 → 绝对值 → 无符号运算 → 符号恢复 → 结果组装。
9.5 (可选)注册 DAGCombine
cpp
setTargetDAGCombine(ISD::SDIVREM);
然后在 PerformDAGCombine 里实现优化逻辑,比如 div(x, 1) → x。
9.6 调试三板斧
bash
# 打印 DAG 变化全过程
llc -debug-only=dagcombiner
# 打印每个 Pass 前后的 DAG
llc -print-before-all -print-after-all
# 只打印和 Lowering 相关的
llc -debug-only=isel
十、关键概念速查表
| 概念 | 一句话 |
|---|---|
| SelectionDAGBuilder | IR → DAG 的第一道门,visit() 是入口 |
| LegalizeTypes | 类型合法化,Promote / Expand / ScalarizeVector |
| LegalizeOperations | 操作合法化,Expand / Promote / Custom / LibCall |
| Custom Lowering | setOperationAction(Custom) + LowerOperation 自己写逻辑 |
| DAGCombine | Worklist 驱动的图重写,迭代到不动点 |
| Chain | 控制依赖边,保证副作用指令不重排 |
| Glue | 粘合边,比 chain 更"紧",强制相邻节点顺序 |
| CopyToReg / CopyFromReg | 物理寄存器的读写节点 |
| TargetLowering | 后端自定义 Lowering 的总入口类 |
十一、写在最后
LLVM 的 Lowering 流程初看很复杂------又是 Legalize 又是 DAGCombine,还有各种 Action 类型------但其实有一条主线贯穿始终:每一步都在把 IR 往目标平台的方向"拉近"。
SelectionDAGBuilder 把 IR 变成 DAG,LegalizeTypes 让类型合法,LegalizeOperations 让操作合法,DAGCombine 反复化简,TargetLowering 用 Custom 回调填补框架覆盖不到的角落。理解了这条链路,写 LLVM Backend 就不会再"跟着教程抄代码但不知道为什么这么写"。
如果这篇文章对你有帮助,建议把视频配合截图一起过一遍,尤其是 SDIVREM 分解那一整段------看着红色箭头一步步标注 DAG 节点的变化,比纯看代码直观得多。