小李第一天到「美味厨房」连锁餐厅实习,主厨老张没有让他去大堂端菜,而是先带他钻进了后厨。小李原以为后厨就是一个大通间,结果越走越惊讶:每道菜都有自己的作业间,作业间里套着一间更小的后厨区;后厨区里不是乱哄哄的一团,而是并排着好几条工位线;工位线之间用传菜口连着,半成品从这条线递到那条线,遇到“食材够不够”的检查口还会自动分岔。老张拍了拍他的肩:“记住这个结构,MLIR 的 Region 与 Block,就是给编译器用的这套后厨图纸。”小李这才明白,原来一段程序在编译器眼里,从来不是一行行平铺的代码,而是一间套一间、一列接一列的“容器迷宫”:操作(Operation)可以挂载区域(Region),区域里装着若干基本块(Block),块与块用跳转边连成控制流图,循环、分支、嵌套作用域全都靠这套结构精确表达。本文就跟小李一起,把这套后厨图纸从头到尾看一遍。

岗位关联

对 AI 从业者的价值:做 AI 编译器、推理引擎或硬件加速器时,读不懂 Region 与 Block 就看不懂算子在 IR 里如何“展开”。掌握这套嵌套容器结构,你才能看懂并编写各种方言的 transformation 与 pass。

目录
  1. 可视化演示
  2. 操作与区域:Op 里藏着一间后厨
  3. 区域与块:后厨里的工位线
  4. 块参数后继:来料槽与传菜口
  5. 控制流图:一张工位路线图
  6. 入口与出口:唯一的入口,多扇安全门
  7. 作用域边界:菜香不出后厨
  8. 嵌套控制流:屋中屋与套餐流水线
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示MLIR中Operation包含Region、Region包含Block的层级嵌套树结构、SSACFG区域中Block参数与终止符驱动的控制流边、以及SSACFG与Graph两类Region语义对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Operation-Region-Block嵌套树 Operation通过Region拥有子IR结构递归嵌套 Region包含有序Block序列构成作用域 Op⊃Region⊃Block 以ModuleOp为根的层级树

操作与区域:Op 里藏着一间后厨

老张先带小李看“红烧肉作业间”。墙上挂着一排调料标签(属性),但真正干活的地方是里间的“红烧肉后厨区”——切肉、焯水、煸炒全部在里间完成。老张说,不是每个作业间都有后厨,比如“打鸡蛋”这种动作,在过道里一磕一倒就完事了,压根不需要里间。

MLIR 里每个节点叫 Operation(操作),简称 Op。有的 Op 像“打鸡蛋”,是原子动作,干完就完;有的 Op 则像“红烧肉作业间”,肚子里可以装一个甚至多个 Region(区域)。Region 就是 Op 内部用来容纳子操作序列的容器,它让一个操作从“单个动作”升级为“一个有内部流程的工作间”。

例如 scf.if 这个结构操作自带两个 Region:条件成立走第一个 Region,不成立走第二个;而 arith.addi 这样的算术操作没有任何 Region。MLIR 规定 Operation 与 Region 的包含关系,就是“作业间”与“后厨区”的关系:

老张总结:“作业间和后厨区的关系,就是 Op 和 Region 的关系。后厨区越多,作业间的流程就越复杂。”小李一边记一边想:那后厨区里面又是怎么布置的呢?老张说,别急,下一步带你认识工位线。

Operation∋Region\text{Operation} \ni \text{Region}
读作“Operation 包含 Region”,表示一个操作可以携带 0 个或多个区域,区域里放着它内部的子流程。
  1. Operation 符号表示 MLIR 中的基本操作实体,相当于一个“作业间”或“小动作”。
    Operation\text{Operation}
  2. 包含符号读作“包含”,强调 Op 是上级容器,Region 是挂在它身上的下级空间。
    ∋\ni
  3. Region 符号表示操作内部挂载的区域容器,里面按顺序放着一批子操作。
    Region\text{Region}

区域与块:后厨里的工位线

走进红烧肉后厨区,小李发现里面并不乱,而是整整齐齐排着三条工位线:接单线、制作线、出菜线。每条线上站着三四个师傅(操作),一个干完传给下一个,顺序不能乱。

MLIR 的 Region 内部,同样不是一团散沙,而是由一个或多个 Block(基本块)组成的。Block 是一连串按顺序排列的操作序列,可以想象成后厨区的“一条工位线”。Region 可以只有一条线(顺序执行),也可以有多条线(多条路径)。它俩的关系写作:

Block 内部的每个位置都是一条具体操作。当控制流进入一个 Region,会从它的某条工位线开始,把线上的操作挨个执行完;如果只有一条线,就别无选择地走到底。这也是为什么很多简单函数在 MLIR 里只有一个 Region、一个 Block:就像小面馆的后厨只有一条线,从头做到尾。

小李脱口而出:“那工位线之间怎么衔接?总不能让师傅端着锅乱跑吧!”老张笑了:“问得好,这就引出了工位线上的来料槽和传菜口。”

Region={ Block0, Block1, …, Blockn−1 }\text{Region} = \{\,\text{Block}_0,\ \text{Block}_1,\ \ldots,\ \text{Block}_{n-1}\,\}
意思是:一个区域本质上是若干基本块的有序集合,块内部的操作用逗号、换行或分号衔接,形成一条条流水线。
  1. Block 下标Block_0、Block_1 等表示区域里编号、排好队的基本块。
    Block0, Block1, …\text{Block}_0,\ \text{Block}_1,\ \ldots
  2. 花括号数学上表示“集合”,这里表示 Region 把若干块收纳在一起。
    { ⋯ }\{\ \cdots\ \}
  3. n 的取值$n$ 可以是 1、2、甚至更大,说明块数不固定。
    n−1n-1

块参数后继:来料槽与传菜口

老张指着接单线和制作线之间的隔墙:墙上有两个来料槽,前面线上处理好的半成品从槽里递过来,后面线上的师傅直接取用。每条工位线的开头还贴着标签:本线需要几样来料。

MLIR 的 Block 也开着这样的“来料槽”——Block 参数(Block Arguments)。当一个 Block 需要上一块传来的值时,就在块头部声明参数;传入方把值“塞进槽里”,接收方在块内直接使用。一条 Block 的结构可以写成:

而“传菜口”就是 terminator(终止操作)。cf.br 是“直通口”:无条件把控制流送到某个后继 Block,同时顺带传参数;cf.cond_br 是“检查口”:根据条件走两条路中的一条。正因如此,Block 与 Block 之间才出现了一条条有向边,控制流图由此而生。

小李恍然大悟:“所以一个工位线不是孤岛,头上有来料槽,尾上有传菜口,前后一接,整条后厨就连成网了。”老张点头:“对,这张网,就是我们的工位路线图。”

Block : ⟨ args, ops, terminator ⟩\text{Block}\ :\ \langle\,\text{args},\ \text{ops},\ \text{terminator}\,\rangle
每个基本块由三部分组成:头部参数(来料槽)、按序排列的操作(工位)、以及末尾的终止操作(传菜口)。
  1. argsBlock 的形参列表,声明本块需要接收哪些外来值,也就是来料槽的清单。
    args\text{args}
  2. ops块内按顺序执行的若干操作,像线上依次排开的师傅。
    ops\text{ops}
  3. terminator块末尾的终止操作,决定本块结束后的去向,因此也是控制流边的来源。
    terminator\text{terminator}

控制流图:一张工位路线图

老张从抽屉里翻出一张皱巴巴的图纸,上面画着后厨所有工位线,每条线是一个方框,方框之间用箭头连着:接单线→制作线→出菜线;遇到“食材够不够”的检查口,箭头分成两路,一路去解冻线,一路直接去煸炒线;制作线末尾还有一条弯回接单线的虚线——那是“加单重做”的循环。

这张图在编译领域叫控制流图(Control Flow Graph, CFG)。Region 里的每个 Block 是图中的一个节点;每个 terminator 产生一条或多条边,指向后继 Block。形式化地写:

有了 CFG,循环和分支就都有了精确的数学表达。分支是“出度超过 1 的节点”,循环是“有一条边绕回自己祖先的环”。这也是后面无数优化 pass 的地基:循环展开、分支合并、尾调用优化,第一步都是在 CFG 上做“看图说话”。

小李看着图纸突然问:“每张图总有起点和终点吧?”老张说:“有,而且规矩很严——每条工位线只能从线头进,但可以有好几个出菜口。”

G=(B, E),E={ (Blocki, Blockj)∣Blocki→Blockj }G = (B,\ E),\quad E = \{\,(\text{Block}_i,\ \text{Block}_j) \mid \text{Block}_i \to \text{Block}_j\,\}
控制流图由节点集合 B(所有基本块)和边集合 E(块间的跳转关系)构成,条件分支产生分岔,循环产生回边。
  1. 节点集合 B图中每个节点就是一个 Block,工位线在图中浓缩成方框。
    BB
  2. 边集合 E每条边代表一次 terminator 跳转,从源块指向目标块,像工位线间的箭头。
    EE
  3. 箭头记号$Block_i \to Block_j$ 表示控制流能够从第 i 块流向第 j 块;回边让图出现环,对应循环。
    Blocki→Blockj\text{Block}_i \to \text{Block}_j

入口与出口:唯一的入口,多扇安全门

老张带小李走到后厨区门口:门上写着“顾客止步,员工入口”。老张说,不管里面有几条线,所有人都只能从这扇门进后厨区;但出去的口子不止一个——正门、侧门、消防门都算出口。这就是后厨区“单入口、多出口”的规矩。

MLIR 对 Region 的约束正是如此:一个 Region 的入口 Block 必须是它的第一个 Block(Block_0),这是唯一的入口;而 Region 的出口可以是任意多个“没有后继”的 Block。用符号表达:

这个设计让编译器的“图纸检查”变得极其简单:想验证一条工位线是否合理,只需从唯一的入口进去,沿所有边走到每一个出口,便知道路径是否全部可达;任何不可达的线,要么是死代码,要么是工程师画错了图。

小李又冒出一个问题:“我听说后厨之间还有规矩,说隔壁甜品间的奶油不能拿到红烧肉间用?”老张竖起大拇指:“你问到点子上了,这叫作用域。”

entry(Region)=Block0,exit(Region)={ b∣succ(b)=∅ }\text{entry}(\text{Region}) = \text{Block}_0,\quad\text{exit}(\text{Region}) = \{\,b \mid \text{succ}(b) = \varnothing\,\}
第一个基本块是区域唯一的入口;没有后继者的基本块都是出口,因此一个区域可以有多个出口。
  1. entry 函数表示“取区域的入口块”,结果固定是第一个块。
    entry(Region)=Block0\text{entry}(\text{Region}) = \text{Block}_0
  2. exit 集合收集所有没有后继的基本块,它们都可以作为区域的出口,对应后厨的各个安全门。
    exit(Region)\text{exit}(\text{Region})
  3. 空后继若某块的后继集合为空,说明控制流到此为止(比如遇到了 return 操作)。
    succ(b)=∅\text{succ}(b) = \varnothing

作用域边界:菜香不出后厨

小李看见甜品间的师傅拎着一盒奶油往红烧肉间走,被领班拦住:“这是甜品间的专用食材,不能带过去!”老张解释说,每个后厨区的食材只在本区可见,跨区使用必须通过来料槽(块参数)显式传递,绝不能顺手就抓。

MLIR 中值的可见性由“定义所在的 Region”决定:一个值定义在哪个 Region,它的作用域就在那个 Region 及其内部嵌套的子区域里;同一 Region 的兄弟区域之间,默认谁也看不见谁。要用,就走 Block 参数显式传。这个规则可以写成:

这条规则保证了 IR 的“卫生”:每个块只看到自己该看的,不会出现跨区域的隐式依赖。对编译器来说,这意味着一块代码可以放心搬运、替换、并行化,不必担心牵一发动全身。这也是为什么高级控制流可以安心地降级成低级分支——值传递路径始终画得清清楚楚。

老张看小李消化得差不多了,抛出最后一个彩蛋:“刚才说的都是'一间后厨'的内部结构。其实后厨区里还可以再套一个小后厨,比如红烧肉间里划出一个熬糖色的独立小间。这种'屋中屋',才真正让编译器表达复杂循环和分支时游刃有余。”

scope(v)⊆Region(def(v))\text{scope}(v) \subseteq \text{Region}(\text{def}(v))
任何值的作用域不超过它定义时所在的区域,跨区域访问必须借助块参数等显式通道。
  1. scope 函数表示值 $v$ 的可见范围,也就是“这盒奶油能在哪几个工位被使用”。
    scope(v)\text{scope}(v)
  2. 定义所在区域$\text{Region}(\text{def}(v))$ 指出这个值诞生于哪个 Region,奶油诞生在甜品间。
    Region(def(v))\text{Region}(\text{def}(v))
  3. 子集符号规定作用域不会超出诞生它的区域边界。
    ⊆\subseteq

嵌套控制流:屋中屋与套餐流水线

老张带小李看总部发来的“大单套餐”指令:如果订单是宴席,就走宴席套餐线;如果是散客,就走小份线。宴席套餐线内部又是一整套完整的工位图——开胃菜区、主菜区、甜点区,各自又有自己的工位线……一层套一层,像俄罗斯套娃。

MLIR 的高级控制流操作正是这样:scf.if 条件操作本身带两个 Region,分别装“成立”和“不成立”两块工位图;scf.for 循环操作带一个 Region 作为循环体,Region 里的入口 Block 声明了循环变量和迭代变量参数。它们在 Region/Block 层面展开后,就是不断递归的嵌套结构:

在真实的 MLIR 方言里,scf.if 在降级到 CFG 方言后,会变成 cf.cond_br 加两个 Block;scf.for 会变成入口块、循环条件检查块、循环体块、回边块四个基本块的组合。但无论方言怎么换,Region 与 Block 这套容器结构始终不变——就像不管菜单怎么变,后厨里永远是一间套一间的作业间加工位线。

参观结束,小李站在后厨门口,回头再看这片“屋中屋”,眼里已经不再是杂乱无章的锅碗瓢盆,而是一张清晰嵌套的工程图纸。老张说:“记住今天看的,以后你写 AI 编译器、调推理引擎,看到 IR 里一排排花括号和块标签,就不会迷路了。”

Op→Region→Block∋Op′→Region′→⋯\text{Op} \to \text{Region} \to \text{Block} \ni \text{Op}' \to \text{Region}' \to \cdots
操作嵌套区域、区域嵌套块、块里又有操作,形成任意深度的递归结构,这就是 MLIR 表达复杂控制流的方式。
  1. 递归箭头箭头表示“容纳”关系层层下钻,从操作进入它内部的区域。
    Op→Region\text{Op} \to \text{Region}
  2. 块中再现操作块内装着一批操作,其中某个操作又自带 Region,于是递归继续。
    Block∋Op′\text{Block} \ni \text{Op}'
  3. 省略号寓意嵌套可以无限深入,区域套区域,块中又生块。
    ⋯\cdots

AI 应用场景

AI 芯片编译器把嵌套循环“摊给”硬件加速器

阶段:中间表示与代码生成

解决的问题:AI 模型里大量嵌套循环(卷积、Attention),传统编译器平铺的指令流难以识别循环边界,无法完成流水线调度和并行核心映射。

具体案例:某 NPU 编译器把 scf.for 表达的组合循环,通过 Region/Block 结构识别出循环嵌套深度,再展开成硬件能并行执行的多个子 Region,最终映射到脉动阵列上。每一个子 Region 就像一个独立后厨,硬件核各自开火互不干扰。

推理引擎里的算子融合 Pass

阶段:图优化与 Pass 流水

解决的问题:多个小算子逐个执行会带来大量显存读写,推理延迟居高不下。

具体案例:TensorRT 与 MLIR 的 fusion pass 通过检查 Region 内 Block 的拓扑顺序,把 conv+bn+relu 三个 op 合并为一个带内部 Region 的自定义 op。合并后数据在片上缓存里直接流转,就像把三个摊位合进一个后厨,半成品不用端出门。

硬件综合中把控制流映射成状态机

阶段:高阶综合(HLS)

解决的问题:把带循环/分支的算法映射为有限状态机时,需要知道每个基本块的入口出口和跳转条件。

具体案例:HLS 工具利用 Region 单入口、多出口的特性,把每个 Block 变成状态机的一个状态,terminator 的分支变成状态转移条件,自动生成 RTL 状态机。后厨的“传菜口”在硬件里就是状态转移线。

Python 演示

python106 行
# -*- coding: utf-8 -*-"""Mini MLIR: 用纯 Python 模拟 Region / Block 的嵌套打印。"""  class Op:    """一个操作: 名字 + 操作数 + 结果 + 可嵌套的 Region 列表。"""     def __init__(self, name, operands=(), results=(), regions=None, suffix=None):        self.name = name        self.operands = list(operands)        self.results = list(results)        self.regions = regions or []        self.suffix = suffix     def emit(self, indent='  '):        head = ''        if self.results:            head += ', '.join('%s: i32' % r for r in self.results) + ' = '        head += self.name        if self.operands:            head += ' ' + ', '.join(str(o) for o in self.operands)        if not self.regions:            head += self.suffix if self.suffix is not None else ' : i32'        lines = [indent + head]        for reg in self.regions:            lines.append(reg.emit(indent + '  '))        return '\n'.join(lines)  class Block:    """基本块: 头部参数(args) + 操作序列(ops)。"""     def __init__(self, label, args=(), ops=()):        self.label = label        self.args = list(args)        self.ops = list(ops)     def emit(self, indent='  '):        arg_str = ', '.join('%s: i32' % a for a in self.args)        lines = ['%s^%s(%s):' % (indent, self.label, arg_str)]        for op in self.ops:            lines.append(op.emit(indent + '  '))        return '\n'.join(lines)  class Region:    """区域: 若干基本块的有序集合。"""     def __init__(self, blocks):        self.blocks = list(blocks)     def emit(self, indent='  '):        lines = ['%s{' % indent]        for b in self.blocks:            lines.append(b.emit(indent + '  '))        lines.append('%s}' % indent)        return '\n'.join(lines)  def build_choose():    """if-else: 两个分支块通过块参数接收值。"""    bb0 = Block('bb0', args=['%flag', '%x', '%y'], ops=[        Op('arith.addi', operands=['%x', '%y'], results=['%sum']),        Op('arith.subi', operands=['%x', '%y'], results=['%diff']),        Op('cf.cond_br', operands=['%flag', '^bb1(%sum)', '^bb2(%diff)'], suffix=''),    ])    bb1 = Block('bb1', args=['%v'], ops=[        Op('func.return', operands=['%v'], suffix=' : i32'),    ])    bb2 = Block('bb2', args=['%v'], ops=[        Op('func.return', operands=['%v'], suffix=' : i32'),    ])    return Op('func.func @pick(%flag: i32, %x: i32, %y: i32) -> i32',              regions=[Region([bb0, bb1, bb2])])  def build_loop():    """scf.for: 循环体是一个 Region, 入口块带循环变量参数。"""    inc = Op('arith.addi', operands=['%acc', '%i'], results=['%next'])    body = Region([        Block('bb_loop', args=['%i', '%acc'], ops=[            inc,            Op('scf.yield', operands=['%next']),        ]),    ])    for_op = Op('scf.for', operands=['%lb', '%ub', '%step', '%init'],                results=['%r'], regions=[body], suffix=' -> i32')    entry = Block('bb0', args=['%n', '%init'], ops=[        Op('arith.constant', operands=['0'], results=['%lb']),        Op('arith.constant', operands=['%n'], results=['%ub']),        Op('arith.constant', operands=['1'], results=['%step']),        for_op,        Op('func.return', operands=['%r'], suffix=' : i32'),    ])    return Op('func.func @sum(%n: i32, %init: i32) -> i32',              regions=[Region([entry])])  def main():    print(build_choose().emit())    print()    print(build_loop().emit())  if __name__ == '__main__':    main()
目的:用纯 Python 类模拟 MLIR 的 Op / Region / Block 三层结构,展示 if-else 分支与 for 循环如何通过嵌套容器和块参数来表达。
  1. self.regions = regions or []一个 Op 可以挂 0 个或多个 Region,统一存成列表,对应“作业间可以没有后厨也可以有很多后厨”。
  2. for reg in self.regions:操作打印完自身信息后,递归打印嵌套区域,这是“屋中屋”结构的核心。
  3. head += self.suffix if self.suffix is not None else ' : i32'控制流指令不追加类型后缀,普通算术指令默认带 : i32。
  4. arg_str = ', '.join(...)把块头部的“来料槽”(块参数)拼成 %name: i32 形式。
  5. Op('cf.cond_br', operands=[..., '^bb1(%sum)', '^bb2(%diff)'])terminator 决定控制流去向并携带参数,像检查口分岔。
  6. body = Region([Block('bb_loop', args=['%i', '%acc'], ...)])循环体就是把一个带循环变量的 Block 塞进 Region。
  7. for_op = Op('scf.for', ..., regions=[body], suffix=' -> i32')带 Region 的操作能完整表达循环,打印时缩进出嵌套结构。

常见误区

误区新人看到 MLIR 文本里的 `{ ... }` 缩进,就以为 Region 和 C 语言的代码块一样,只是个“括号作用域”。

正解Region 本质是 Op 内部容纳子操作序列的容器,是可以被单独遍历、改写、替换的一等公民。就像后厨区是一个真正能走进去的工作空间,而不只是墙上的两个符号。C 的花括号没有自己的“身份”,Region 却有类型和结构,可以被 pass 独立操作。

为何常见:文本打印太像花括号代码,加上很多语言都有类似语法,容易先入为主。

误区看到“block“就想到”一段代码“或”打包的一组语句”,很多人以为基本块就是一条大指令。

正解Block 是一个“操作序列 + 末尾终止操作”的结构,内部至少可以装多条 op;它是控制流图上的节点,尾部 terminator 决定它连向谁。就像后厨里的一条工位线,由多个工位排成,而不是一个单独的灶台。

为何常见:英文 block 在日常英语里就是“块/一段”,初学者望文生义。

误区觉得“控制流结构”就得有很多块,至少两个以上,否则不叫流程。

正解Region 可以只有 1 个 Block,甚至 0 个 Block。顺序执行的函数体就是一个 Block 的 Region;某些场景下 Region 也可以为空占位。块的数量由操作的功能决定,与“是否合法”无关。

为何常见:教材多用 if/for 举例,基本都展示多块结构,让人误以为块多是前提。

误区既然控制流边可以指向任何块,那入口块也是任意的,第一个块没什么特殊。

正解MLIR 规定 Region 的入口只能是第一个 Block。外部控制流进入 Region 时默认从它开始;其它块只能通过块间边到达。这个“单入口”约定让 pass 能快速定位分析起点,就像后厨区只有一个员工入口,侧门消防门只能出不能进。

为何常见:看到 CFG 上边可以指向任意块,就以为入口没有特殊地位。

面试题

Q1. 在 MLIR 中,一个 Operation 可以包含几个 Region?

单选题 · easy

  • A.最多一个
  • B.零个或多个← 正确答案
  • C.至少一个
  • D.恰好一个

答案:B。MLIR 的 Operation 可以携带 0 个或多个 Region。就像作业间可以没有后厨,也可以有多个后厨区;算术操作(如 arith.addi)不带 Region,而 scf.if 带两个 Region。A、C、D 都限制了数量,不符合自由度。

Q2. Region 的入口 Block 是哪一个?

单选题 · easy

  • A.最后一个
  • B.任意一个
  • C.第一个块← 正确答案
  • D.每个块都是

答案:C。Region 规定第一个 Block 是唯一的入口,外部控制流进入 Region 时从它开始。就像后厨区只有一个员工入口,不是哪个门都能进。因此 C 正确。

Q3. Block 内部紧挨着排列的是什么?

单选题 · easy

  • A.只有一条指令
  • B.操作序列← 正确答案
  • C.表达式列表
  • D.一串字符串

答案:B。Block 是一条“工位线”,里面装着一连串按顺序执行的操作(Op)。它可以容纳多条指令,不是单个指令;操作序列对应后厨里依次排开的师傅。

Q4. 控制流图中,Block 之间的“边”由什么决定?

单选题 · mid

  • A.块参数的个数
  • B.作用域大小
  • C.终结操作决定← 正确答案
  • D.操作的类型

答案:C。每条边都来自 Block 末尾的终结操作(terminator),如 cf.br、cf.cond_br。terminator 像传菜口决定半成品送往哪条线,块参数只负责传值,不决定去向。

Q5. Block 参数的主要作用是什么?

单选题 · mid

  • A.声明局部变量
  • B.接收传入的值← 正确答案
  • C.保存常量引用
  • D.标注结果类型

答案:B。Block 参数(Block Arguments)是块头部的“来料槽”,专门用来接收前驱块传来的值。声明局部变量是块内操作的工作;常量引用和结果类型也与 Block 参数无关。

Q6. 下列关于 Region 的说法,正确的有?

多选题 · mid

  • A.只能含一个块
  • B.可以含多个块← 正确答案
  • C.只有一个入口← 正确答案
  • D.块块必须相连

答案:BC。Region 可以含 1 个或多个 Block,且规定第一个块为唯一入口(单入口),但可以有多个出口。D 错在“必须相连”——Region 允许存在无后继的出口块,也不需要所有块都相互连通。

Q7. scf.if 在 MLIR 中会展开为怎样的结构?

单选题 · hard

  • A.一个独立区域
  • B.分支与合流块← 正确答案
  • C.一个单独块
  • D.全局变量表

答案:B。scf.if 携带两个 Region,分别对应条件成立与不成立的分支,展开后是一组分岔与合流的 Block 簇,配合终结操作构成完整控制流。它绝不是单块结构,也不依赖全局变量。

Q8. 循环(scf.for)在 Region/Block 层面如何体现?

多选题 · hard

  • A.循环体是一个区域← 正确答案
  • B.块参数承载计数← 正确答案
  • C.回边构成循环← 正确答案
  • D.循环体不能嵌套

答案:ABC。scf.for 的循环体是一个 Region,入口块通过块参数接收循环变量和迭代累积值;降级到 CFG 后,循环靠回边(从循环体末尾跳回条件检查块)形成环。D 错误,循环体 Region 里完全可以嵌套新的循环。

Q9. 值的作用域边界主要由什么确定?

单选题 · mid

  • A.所在的基本块
  • B.所在的区域← 正确答案
  • C.所在的操作
  • D.全局都不受限

答案:B。一个值定义在哪个 Region,它的可见域就以该 Region 为边界;兄弟区域默认不可见,跨区域必须通过块参数显式传递。这就像奶油只能在自己的后厨区使用,不能顺手拿到别的区。

Q10. 关于嵌套 Region 结构,下列说法正确的有?

多选题 · extreme

  • A.区域可以无限嵌套← 正确答案
  • B.外层看不到内层值← 正确答案
  • C.跨块传值用参数← 正确答案
  • D.内层必须放首块

答案:ABC。Op 套 Region、Region 装 Block、Block 里又可以有带 Region 的 Op,理论上可无限嵌套;内层定义的值对外层不可见(作用域封闭);跨块传值通过 Block 参数完成。D 错误——内层 Region 并没有“必须放在首块”的限制。