Region 与 Block:IR 的控制流骨架
前面几章讲了 Operation 和 Value 的 C++ 表示——但单独一个 Op 做不了什么。真正让 IR 成为”程序”的是 Region 和 Block 提供的控制流骨架。
如果说 Operation 是 MLIR 的血肉,Region 就是骨骼。不理解 Region 能在多大程度上嵌套和隔离控制流,就无法理解 MLIR 为什么能表达从张量运算到硬件逻辑的完整降阶路径。
一、Region 不是 BasicBlock
先从 LLVM IR 的基本块模型开始。在 LLVM 中:
define i32 @max(i32 %a, i32 %b) {
entry:
%cmp = icmp sgt i32 %a, %b
br i1 %cmp, label %then, label %else
then:
ret i32 %a
else:
ret i32 %b
}Function 包含 BasicBlock
的列表,BasicBlock 包含
Instruction
的列表。结构是扁平的——所有基本块都在同一个作用域中,通过
br 指令连接。
MLIR 中,控制流的嵌套在 Region 层:
func.func @max(%a: i32, %b: i32) -> i32 {
%cmp = arith.cmpi sgt, %a, %b : i32
%result = scf.if %cmp -> i32 {
scf.yield %a : i32 // ← 这个 Block 在 scf.if 的 then-Region 内部
} else {
scf.yield %b : i32 // ← 这个 Block 在 scf.if 的 else-Region 内部
}
return %result : i32
}
关键区别:
flowchart LR
subgraph llvm["LLVM IR(扁平)"]
E1[entry] --> T1[then]
E1 --> E2[else]
T1 --> M[merge]
E2 --> M
end
subgraph mlir["MLIR(嵌套 Region)"]
IF["scf.if"] --> R1["then Region"]
IF --> R2["else Region"]
end
| 维度 | LLVM IR | MLIR |
|---|---|---|
| 控制流表示 | 扁平 CFG(所有 BB 同级) | 嵌套 Region(控制流结构在 IR 结构中可见) |
| 值汇合 | phi 指令(显式) | Block 参数(隐式绑定) |
| 循环 | 无特殊表示(br 回跳) | scf.for / affine.for(结构化
Op + Region) |
| 分支 | br(无条件)/
br i1(条件) |
scf.if(结构化 Op + 两个 Region) |
Region 的嵌套不是语法糖——它是语义上的边界。一个 Region 内的值不能逃逸到外层 Region,控制流也不能跳过 Region 边界(除非通过明确的 terminator)。
二、Block 参数:MLIR 对 phi 节点的答案
LLVM IR 用 phi 节点处理控制流汇合:
; LLVM: phi 在 BB 开头,显式列出每个前驱 BB 的值
bb.merge:
%result = phi i32 [ %a, %then ], [ %b, %else ]MLIR 用 Block 参数实现同样的语义:
// MLIR: 值通过 successor 绑定到目标 Block 的参数
^bb1(%arg0: i32): // %arg0 是 Block 参数
// 使用 %arg0 ...
跳转时绑定参数:
cf.br ^bb1(%a : i32) // 从 then-路径跳转,绑定 %a
cf.br ^bb1(%b : i32) // 从 else-路径跳转,绑定 %b
这两种表示在语义上是等价的,但 Block 参数有几个工程优势:
1. 不需要维护前驱列表:phi 指令中每个值都与一个前驱 BB 绑定——这意味着修改 CFG 必须同时更新 phi 节点。Block 参数不需要前驱信息——数据流和控制流的耦合更松。
2. 结构化控制流中的隐式绑定:在
scf.if 中,scf.yield
自动将值绑定到 if Op 的 result(即其 enclosing Region
的出口),不需要在 IR 中显式写绑定关系:
%result = scf.if %cond -> i32 {
scf.yield %a : i32 // 隐式绑定到 %result
} else {
scf.yield %b : i32 // 隐式绑定到 %result
}
// 这里 %result 可以直接使用——不需要显式 phi
3. 分析更简洁:确定”哪些值汇合到哪个 Block”只需要看 Block 的签名,不需要扫描 phi 节点。
Block 参数的设计源于 Swift Intermediate Language (SIL) [Groff & Lattner, 2014],在 MLIR 中得到推广。从 LLVM IR 转过来的工程师最初会觉得不习惯,但习惯后会发现在结构化控制流下 Block 参数比 phi 更好处理。
三、Region 的两种类型
MLIR 定义了两种 Region 类型 [MLIR Language Reference]:
3.1 SSACFG Region
SSACFG Region 允许多个 Block 和 CFG
边(cf.br、cf.cond_br)。这是 MLIR
中表达任意控制流的底层机制:
func.func @loop(%n: index) -> i32 {
%c0 = arith.constant 0 : i32
%c0_index = arith.constant 0 : index
%c1_index = arith.constant 1 : index
cf.br ^loop_header(%c0, %n : i32, index)
^loop_header(%acc: i32, %i: index):
%done = arith.cmpi eq, %i, %c0_index : index
cf.cond_br %done, ^exit(%acc : i32), ^body(%acc, %i : i32, index)
^body(%acc2: i32, %i2: index):
%new_acc = arith.addi %acc2, %acc2 : i32
%next_i = arith.subi %i2, %c1_index : index
cf.br ^loop_header(%new_acc, %next_i : i32, index)
^exit(%final_acc: i32):
return %final_acc : i32
}
SSACFG Region 是最通用的表示,但它的自由度也意味着分析难度更高——你必须重建循环结构、支配树等。适合作为”所有结构都解构后的最低共同表示”。
3.2 Graph Region(单 Block Region)
Graph Region 只有一个 Block,不允许 CFG 边。这是结构化控制流 Op 中使用的 Region 类型:
scf.for %i = %lb to %ub step %step {
// 这里只有一个 Block——这就是 Graph Region
%val = arith.addi %i, %c1 : index
scf.yield
}
Graph Region 保证了 Region 内部的执行是单入口、线性、单出口的——不存在内部跳转或分支。这让数据流分析变得 trivial:Region 内的值定义顺序就是执行顺序,不需要构建 CFG 或支配树。
3.3 为什么需要两种?
SSACFG Region 和 Graph Region 的分工对应了 MLIR 的”渐进降阶”哲学:
- 高层方言(tensor、linalg)使用 Graph Region——分析简单、语义清晰。
- 中层方言(affine、scf)使用 Graph Region——仍然保持结构化,便于调度。
- 低层方言(cf)使用 SSACFG Region——完全展开的 CFG,准备映射到机器代码。
tensor (数学运算,无显式循环) → Graph Region
↓
linalg (结构化操作,如 matmul) → Graph Region
↓
affine (仿射循环,可分析) → Graph Region
↓
scf (结构化控制流,for/if) → Graph Region
↓
cf (通用 CFG,br/cond_br) → SSACFG Region
↓
llvm (LLVM IR 方言) → SSACFG Region
四、Region 作为 Lambda 式抽象
Region 在 MLIR 中扮演的角色远不止 BasicBlock 的容器——它可以实现 Lambda-lifting 式的抽象边界。
考虑 scf.for 的定义:
scf.for %i = %lb to %ub step %step iter_args(%iter = %init) -> (i32) {
%new_val = arith.addi %iter, %c1 : i32
scf.yield %new_val : i32
}
scf.for 的 body Region
像一个闭包:它捕获外部值(%c1),接收参数(%i
和
%iter),在每次迭代时产生一个新值(scf.yield
的参数)。Region 的隔离保证了 body
内的值不会泄露到循环外面——只有通过 scf.yield
显式传递的值才能逃逸。
Region 也用于建模高层抽象:
| Op | Region 的语义 |
|---|---|
func.func |
函数体 Region——捕获全局符号,定义局部 SSA 值 |
scf.if |
两个 Region(then/else)——值通过 scf.yield
汇合 |
scf.for |
循环体 Region——值通过 scf.yield
跨迭代传递 |
scf.while |
条件 Region + 循环体 Region——前置条件+迭代 |
gpu.launch |
并行计算 Region——捕获共享内存和线程索引 |
linalg.generic |
结构化计算 Region——捕获输入/输出 tensor,定义逐元素计算 |
每种情况下,Region 的作用是:限制值的可见性范围、定义计算的语义边界、提供可替换的抽象单元。
五、Region 隔离与 SSA 值的可见性
SSA 值的可见性遵循嵌套作用域规则:
外层 Region/Block
├── 定义了 %a, %b → 在所有内层可见
│
├── 内层 Region 1
│ ├── 可以使用 %a, %b
│ └── 定义了 %x → 仅在 Region 1 内可见
│
└── 内层 Region 2
├── 可以使用 %a, %b
├── 不能使用 %x(不在作用域内)
这个规则的意义:一个 Region
内部的值无法逃逸——除非通过 terminator(如
scf.yield、func.return)显式传递。对于编译器优化,这意味着:
- 死代码消除:如果 Region 内的值没有通过 terminator 传递给外部,且没有副作用,它就是死的。
- Region 替换:如果两个 Region 产生相同的输出(通过 terminator 传递),它们就是等价的——整个 Region 可以作为一个单元被替换。
- Inlining:将函数体 Region 展开到调用点,需要处理的是参数的重新绑定——这与 lambda 的 beta-reduction 是同一类操作。
六、遍历 Region 的 C++ 模式
回到 C++。遍历嵌套的 Region 结构使用 Walk 模式:
// 访问 Module 中的所有 Operation(深度优先)
moduleOp.walk([](Operation *op) {
// 对每个 Op 做操作
});
// 访问特定类型的 Op
moduleOp.walk([](scf::ForOp forOp) {
// 只处理 scf.for
Region &body = forOp.getRegion();
Block &entryBlock = body.front();
// ...
});
// 访问 Region 中的所有 Block
region.walk([](Block *block) {
for (BlockArgument arg : block->getArguments()) {
// 处理 Block 参数
}
});
// 在特定 Region 中遍历
Operation *parentOp = region.getParentOp();
if (auto funcOp = dyn_cast<func::FuncOp>(parentOp)) {
// 这是一个函数体 Region
}七、与经典 SSA 和 CPS 的比较
MLIR 的 Region + Block 参数设计同时受到了两种传统的启发:
经典 SSA(Cytron et al., 1991):所有的值都是静态单赋值的,通过 phi 节点汇合控制流。MLIR 保留了 SSA 的核心不变性(每个值在程序文本中只定义一次),但用 Block 参数替代了显式的 phi 指令。
Continuation-Passing Style (CPS):将控制流”接下来执行什么”表示为 cont 参数。Region 可以被视为一种轻量级的 CPS——一个 terminator 描述了”接下来跳到哪个 Block,带什么参数”。但 MLIR 没有像真正的 CPS 那样将所有控制流都转为函数调用——它保留了基本块和控制流图的表示,只是加了结构化包装。
Lambda 演算与闭包转换:Region 捕获外部值的能力类似闭包。方言转换中的一个常见操作是”Region outline”——将一个 Region 提取为独立的函数。反之,“inlining”——将函数调用展开,函数体 Region 嵌入调用点。这两项操作在 MLIR 中有框架级支持。
八、本篇后续
至此,MLIR 核心概念(Operation、Value、Type、Attribute、Region、Block、ODS)全部覆盖。下一篇进入 Part 3——Pass 管理与分析:如何用这些数据结构构建编译流水线。
参考资料
官方文档(A 级)
- MLIR Language Reference — https://mlir.llvm.org/docs/LangRef/
- MLIR Region & Block documentation — https://mlir.llvm.org/docs/LangRef/#regions
论文(A 级)
- Lattner, C. et al. MLIR: A Compiler Infrastructure for the End of Moore’s Law. arXiv:2002.11054, 2020.
- Cytron, R. et al. Efficiently Computing Static Single Assignment Form and the Control Dependence Graph. TOPLAS, 1991.
- Groff, J. & Lattner, C. High-level IRs and Optimizations. LLVM Developers’ Meeting, 2014.(SIL 与 Block 参数设计)
源码(A 级)
mlir/include/mlir/IR/Region.hmlir/include/mlir/IR/Block.h
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【编译器与 MLIR】操作、方言与 IR 的 C++ 表示
深入 Operation、Op、Value、Block、Region 的 C++ 内存布局与继承体系:CRTP 模板包装、SSA 值的两种来源、Use 链表的遍历方法。这是后续所有 Pass 写作的基础。
【编译器与 MLIR】编译器的挑战与 IR 的裂变
从三阶段编译器局限出发,串联 Halide、XLA、TVM 的 IR 裂变,说明 DSA 与 AI 编译器为何需要 MLIR 这类可组合的多层 IR 框架。
【编译器与 MLIR】MLIR 全景图与设计哲学
从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。
【编译器与 MLIR】环境搭建与第一个 MLIR 程序
从零构建 LLVM/MLIR 工程,用 mlir-opt 理解 .mlir 文本表示,运行规范化 Pass 并逐行解读转换结果,建立从命令行到 IR 变换的直觉。