想象一间正在为满汉全席备菜的后厨:洗菜师傅把菜洗净,切配师傅把菜切成标准大小的肉片,腌制师傅提前入味,最后热炒师傅颠勺出锅。每一位师傅都必须等上一道工序完成才能开工,而总厨手上有一张写满需求的长单——爆炒需要腌制好的牛肉,腌制需要切好的肉块,切配需要洗干净的食材。只要排错一个顺序,后厨就会乱成一锅粥。MLIR 编译器中的 Pass 依赖管理,正是这张后厨需求单的工程化翻版:Pass 是师傅,中间表示(IR)是案板上的食材,PassManager 是总厨,而“谁等谁做完”的约定就是 Pass 依赖。本文就带着这张后厨地图,从基础概念一路走到工程实践,帮你把这条编译流水线看得明明白白。
掌握 Pass 依赖管理,是设计高效 MLIR 编译管线的核心技能。它让你真正驾驭 AI 编译器的优化流程,快速定位“优化不生效、结果不正确”的疑难问题,是 AI 编译器工程师的必备素养。
目录
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
Pass基础认知:认识后厨里的每位师傅
后厨里没有一个人能从生牛腩直接变出红烧肉。洗、切、腌、炒,每一步都隔着一位师傅和一双手。MLIR 编译器同样如此:从输入模型到最终机器码,中间表示(IR)要经历少则几十、多则数百道工序,每一道工序就是一个 Pass(通行证,代表一次编译变换)。
Pass 是 MLIR 中最基本的执行单元,它在 IR 上做一次完整的遍历,然后实施一种变换或收集一项信息。把所有 Pass 按顺序串起来,就是编译管线(pass pipeline)。负责注册、排序和执行这条管线的是 PassManager——它就像后厨总厨,拿着排班表决定哪位师傅在哪个时间点上工。
按职责划分,Pass 有两大家族。转换 Pass(Transform Pass)会动手改写 IR:死代码消除、循环展开、算子融合都属于这一类。分析 Pass(Analysis Pass)只看不动:统计指令条数、推导张量形状、分析活跃变量,它们产出的分析结果由 AnalysisManager 统一管理,供后续 Pass 按需取用。
把每个 Pass 想象成一位师傅,管线的正确性就取决于师傅们的顺序是否合理。理解“Pass 的输入输出契约”是读懂依赖关系的第一步——后文我们会看到,正是这些朴素的先后关系,延伸出了依赖声明、分析失效、拓扑调度等一整套工程问题。
\circ 就是后厨里“这道工序做完接着下一道”的箭头。- 输入 IR(生食材)$\text{IR}_{\text{in}}$ 是进入管线前未经处理的中间表示,如同刚从市场买回的食材。
- 逐个 Pass(加工工序)$p_1, p_2, \dots, p_n$ 是按管线顺序注册的 Pass,前一个的输出恰好是后一个的输入。
- 复合算子(工序接力)$\circ$ 表示函数复合,从右往左依次执行,把 n 道工序串成一条流水线。
- 最终 IR(成品菜)$\text{IR}_{\text{out}}$ 是全管线执行完毕后的 IR,交给下一阶段或后端代码生成。
依赖关系类型:谁需要谁的半成品
腌制师傅需要知道“肉切好了没有”,爆炒师傅需要“腌制完成的提醒”。有些师傅把需求白纸黑字写进需求单(显式依赖),有些师傅只是默默盯着案板,等半成品出现就开工(隐式依赖)。依赖,就是师傅之间心照不宣的协作约定。
MLIR 里 Pass 之间的依赖主要有三种形态。最直白的是显式依赖:一个 Pass 在代码里声明“我需要 XX 分析”,而该分析由另一个 Pass 产出,于是后者必须排在前面。这种依赖看得见、摸得着,调度器能直接读出来。
第二类是隐式依赖,它不通过分析接口表达,而是由 IR 结构暗含。例如同一个 Region 内的 Pass 执行有默认先后、方言转换必须先于方言优化,这些约定写不进需求单,但调度器必须尊重。第三类是顺序约束:调度器根据依赖边推导出的全序要求,即“生产者必须先于消费者”。三种依赖共同决定了 Pass 的合法排布空间。
一张写着明确需求单的后厨,永远不会出现“爆炒师傅上了灶才发现肉还没切”的尴尬。显式依赖就是那张需求单——它让调度器在开工前就能看穿每一份半成品的流向。
p 的直接依赖,是那些产出 p 所需分析结果的 Pass 们。就像爆炒师傅的需求单上写着“腌制师傅的成品”。- Pass 全集(后厨名册)$\mathcal{P}$ 表示当前管线上所有 Pass 的集合。
- 直接依赖(需求单)$\text{dep}(p)$ 返回 Pass $p$ 的直接依赖列表。
- 被依赖的 Pass(上游师傅)$q$ 是某个产出分析结果的 Pass,且 $q$ 必须先于 $p$ 执行。
- 分析结果消费(半成品交接)“$p$ 需要 $q$ 产出的分析结果”说明 $p$ 的输入之一来自 $q$,两者间形成一条依赖边。
分析管理机制:后厨信息台账与保鲜期
后厨里总有一本台账,记录着“今天备了几道菜”“哪块肉腌了多久”。光有人记录还不够,食材一旦被改动,台账上相关条目就要作废重写。AnalysisManager(分析管理器)就是这本台账的保管人。
AnalysisManager 负责分析结果的创建、缓存与失效。每条分析结果都带着一个版本号——它诞生时 IR 的版本。只要 IR 没变,版本一致,后续 Pass 可以直接把缓存结果拿走,省去一次重新计算。
当某个转换 Pass 修改了 IR,IR 版本号加一,旧的分析结果立刻“过期”。此时 AnalysisManager 会参照 Pass 的保留声明(preservedAnalyses)决定生死:Pass 明确说“我没动分析 A 依赖的结构”,A 就继续保鲜;没声明的一律默认失效,等下一次被请求时重新计算。保留声明诚实与否,直接决定下游 Pass 拿到的是鲜肉还是馊肉。
台账的本质是“用空间换时间”,但它的前提是版本号对得上。后厨里谁改动了食材都要喊一嗓子,编译器里则是 Pass 每次修改 IR 都自动推进版本号——这就是分析失效机制存在的意义。
a 是否有效,取决于第 i 个 Pass 是否声明保留了它。声明保留 = 食材还在保鲜期;没声明 = 默认过期。- 分析对象(食材信息)分析 $a$ 指某项只读信息,如指令统计、张量形状推导。
- 当前 Pass(某道工序)$S_i$ 是管线中正在执行的第 $i$ 个 Pass,它可能修改 IR。
- 保留声明(不受影响的方面)$\text{preserved}(S_i)$ 是 Pass 自己声明的“我保证没改坏”的分析集合;若 $a$ 在其中则有效。
- 失效兜底(默认保鲜期结束)未声明的分析一律视为失效,下次被请求时必须重算。
依赖调度原理:拓扑排序就是后厨排班
总厨拿到一堆需求单后,会先画一张“谁依赖谁”的关系图,然后让没有前置工序的师傅先开工,收工后挨个通知等他的师傅解锁。这种排班方法,计算机科学里叫拓扑排序。
调度时,PassManager 把每个 Pass 看作顶点,把依赖关系看作有向边,构建一张依赖图 $G=(V,E)$。随后用 Kahn 算法求解:统计每个顶点的入度(它依赖的前置 Pass 数量),入度为 0 的顶点先入队。
每弹出一个顶点,就把“依赖它的那些顶点”的入度减一;谁减到 0,谁就进入下一批待开工队列。如果所有顶点都能按此规则被弹出,队列给出的顺序就是合法管线。反之,若队列提前见底而仍有顶点未弹出,说明存在循环依赖——A 等 B、B 等 A 的僵局——PassManager 会直接报错并拒绝构建管线。
拓扑排序解决的核心问题是“先做谁”。就像后厨永远从洗菜开始、从没有前置工序的环节开始,编译器的第一批 Pass 也永远是那些谁都不依赖的“洗菜师傅”。
- 依赖边(协作要求)$(u,v) \in E$ 表示 Pass $u$ 必须先于 Pass $v$ 完成。
- 执行序号(排班位置)$\text{order}(u)$ 是调度完成后 Pass $u$ 在管线中的位置。
- 全序条件(排班表合法)所有依赖边都满足“生产者在下游之前”,这条公式就是合法调度表的判定标准。
- 残缺即环(死锁信号)若无法为全部顶点找到满足条件的顺序,说明依赖图中存在环,调度必须终止。
性能优化策略:让后厨更快出菜
出菜速度是餐厅的生命线。聪明的后厨不会把每道菜原料都提前备齐——没人点的菜备了也是白备;也不会重复切同一块肉——切一次,所有人都来取。MLIR 管线的性能,同样藏在这三个原则里。
第一,惰性计算(Lazy Evaluation):分析结果只在被 Pass 首次请求的那一刻才计算,不请求就不算。第二,缓存复用:未被失效的分析结果,后续 Pass 直接命中,零计算成本。第三,增量失效:失效粒度越小越好——一个 Pass 只改了一个操作,却让所有分析整体作废,代价是灾难性的。
因此 Pass 必须诚实填写 preservedAnalyses,把“我真的没碰过”的分析保留下来。此外,把频繁成对出现的小 Pass 合并成一个(Pass 合并 / 桶化),能减少整趟 IR 遍历和簿记开销,这是实践中性价比最高的优化手段之一。性能优化的本质,就是让指示失效的那个开关尽量保持关闭。
后厨提速靠的是“别重复备料、别提前备料、别把整桌菜重做”,编译器提速靠的则是“别重复计算、别提前计算、别让分析整体失效”。三个原则一一对应,这就是 Pass 性能优化的心法。
- 总耗时(出菜总时间)$T_{\text{total}}$ 是整条管线从开始到结束的总耗时。
- Pass 本体开销(工序固定耗时)$t_{\text{exec}}(p)$ 是 Pass $p$ 自身变换的耗时,与缓存无关。
- 指示函数(是否需要重新备料)$\mathbb{I}[\neg \text{valid}(a)]$ 在分析失效时取 1、有效时取 0,决定是否计入重算开销。
- 重算代价(重新备料的代价)$t_{\text{recompute}}(a)$ 是重新计算分析 $a$ 的耗时,优化目标是让指示函数尽量为 0。
故障诊断排查:后厨出乱子怎么办
再训练有素的后厨也会出乱子:两位师傅互相等对方开工(循环依赖)、用了发馊的信息做菜(分析过期)、偷偷依赖没写进需求单的半成品(隐性依赖缺失)、以及越干越慢的出菜速度(性能退化)。
这些故障的根源高度一致:依赖信息不完整,或者失效信息不准确。循环依赖是依赖图里有环,无法拓扑排序;分析过期是 Pass 违反了保留声明;隐性依赖缺失是拆散 Pass 时把暗含的顺序弄丢了;性能退化则是失效范围被过度放大。
排查时,MLIR 提供了成套工具:用 --debug-only=pass-manager 打印依赖图的构建过程,用 --mlir-print-ir-before-all / after-all 逐步比对 IR 变化,用分析失效日志观察哪些分析被反复重算。定位到根因后:环,通过抽取公共分析打破;过期,通过诚实声明保留修复;隐性依赖,通过合并 Pass 或显式声明补上。
后厨出了乱子,第一件事是翻台账和排班表;编译器出了乱子,第一件事同样是看依赖图和失效日志。数据不会说谎,缺失的那条边往往就藏在日志最不起眼的角落。
- 依赖图(协作网络)$G=(V,E)$ 中顶点 $V$ 是 Pass,边 $E$ 是依赖关系。
- 拓扑排序(排班算法)$\text{TopoSort}(G)$ 是对 $G$ 求出的一个合法执行序列。
- 顶点覆盖比较(人数点齐)排序结果包含的顶点数等于 $|V|$ 时,说明每个 Pass 都找到了合法位置。
- 有环即缺人(死锁)若排序数小于 $|V|$,说明残留顶点形成环,调度无法完成。
工程实践指南:星级后厨的运营手册
星级后厨都有自己的运营手册:需求单必须写全,新菜先小范围试做,每道工序留可追溯的记录。Pass 依赖管理的工程化,同样是这三条铁律的展开。
第一条,依赖声明:在 Pass 中显式声明所需分析,把“我依赖谁”变成可被调度器静态检查的事实,而不是藏在代码注释里的潜规则。第二条,管线设计:把稳定复用的 Pass 序列封装成 pipeline builder 函数,像套餐一样整体下发,避免每个调用方各自拼装、顺序漂移。
第三条,验证与调试:用 --pass-pipeline 精确指定某段管线做局部验证,用 IR 打印逐步比对优化前后差异,每次只改一个 Pass 再观察效果。最佳实践可以浓缩为四句话:显式声明最小依赖集;诚实声明保留分析;小步验证、频繁比对;把成对出现的 Pass 合并成桶。
运营手册的最后一条总是同一个提醒:对未知保持敬畏。后厨如此,编译管线亦如此——一条依赖清晰、声明诚实的 Pass 管线,才是能长期维护、快速迭代的可靠生产线。
p 声明的每个需求都能在实际可得的分析结果中找到。料齐了,才准开工。- 真实需求(需求单上的条目)$\text{required}(p)$ 是 Pass $p$ 在代码里声明的分析依赖集合。
- 当下可得(案板上的料)$\text{available}(p)$ 是调度执行到 $p$ 时,实际上已经产出且有效的分析集合。
- 包含关系(料齐才能开工)每个需求单品都能在案板上找到,是 $p$ 可以安全开工的充要条件。
- 静态验证(开工前检查)调度器在排产阶段用该条件检查管线合法性,避免运行时才发现缺料。
AI 应用场景
为 AI 芯片定制编译器管线
阶段:模型编译与代码生成
解决的问题:从 ONNX/TensorFlow 模型到 NPU 指令要经过几十个 Pass。若依赖声明混乱,shape 推导分析被无谓失效,编译时间膨胀数倍甚至产出错误代码。Pass 依赖管理让每个 Pass 明确知道“上游谁给我备料、下游谁要我的半成品”。
具体案例:某团队用 MLIR 为 NPU 构建编译器:在 TOSA → Linalg → 内核方言的每段管线上,显式声明 shape 分析依赖,并让每个小 Pass 诚实汇报保留分析。结果编译时间下降 40%,且优化结果全程可复现。
大模型推理引擎的算子融合
阶段:推理图优化
解决的问题:Attention 算子融合需要内存布局分析结果来判断能否安全合并。若依赖声明缺失,融合 Pass 拿到的 shape 信息是过期的“馊肉”,融合后的计算图语义可能被悄悄破坏。
具体案例:在自研推理引擎中,融合 Pass 显式依赖“内存布局分析”,并在 IR 版本变化时自动触发失效重算。长序列推理场景下,融合正确率从 92% 提升到 100%,端到端时延下降 18%。
增量编译基础设施
阶段:开发期重编译
解决的问题:训练框架更新一个小算子,整个模型就要全部重编译。借助 Pass 依赖图的版本号机制,可以精准定位受影响的 IR 区间,只重跑被影响的子管线。
具体案例:某 MLIR 平台团队搭建增量编译服务:分析结果带版本号标记,IR 未变动的区域直接复用缓存,MR 反馈时间从 20 分钟降到 3 分钟,显著提升开发迭代效率。
Python 演示
class IRBlock: """模拟编译器中间表示(IR):一串指令,带版本号用于失效判断""" def __init__(self, ops): self.ops = list(ops) self.version = 0 def mutate(self, transform_fn): self.ops = transform_fn(self.ops) self.version += 1 class AnalysisResult: """分析结果:记录数据与产生时的 IR 版本号(相当于食材保鲜期)""" def __init__(self, data, version): self.data = data self.version = version class AnalysisManager: """模拟 MLIR 的 AnalysisManager:负责分析结果的缓存与失效判断""" def __init__(self): self.cache_map = {} def obtain(self, name, ir, recompute_fn): result = self.cache_map.get(name) if result is not None and result.version == ir.version: print(" 命中缓存:", name, "值为", result.data) return result.data print(" 缓存失效,重新计算:", name, "当前版本", ir.version) data = recompute_fn(ir) self.cache_map[name] = AnalysisResult(data, ir.version) return data class CompilerPass: """一个编译 Pass:名称、前置 Pass 依赖、所需分析、变换函数""" def __init__(self, name, deps, needs, transform_fn): self.name = name self.deps = deps self.needs = needs self.transform_fn = transform_fn def topological_order(pass_list): """Kahn 算法:按 Pass 依赖拓扑排序,同时检测循环依赖""" indegree_map = {} dependents_map = {} pass_by_name = {} for p in pass_list: pass_by_name[p.name] = p indegree_map[p.name] = len(p.deps) for dep in p.deps: dependents_map.setdefault(dep, []).append(p.name) ready = [p for p in pass_list if indegree_map[p.name] == 0] ordered = [] while ready: current = ready.pop(0) ordered.append(current) for next_name in dependents_map.get(current.name, []): indegree_map[next_name] -= 1 if indegree_map[next_name] == 0: ready.append(pass_by_name[next_name]) if len(ordered) != len(pass_list): raise RuntimeError("存在循环依赖,无法调度 Pass") return ordered def count_ops(ir): """分析函数:统计指令条数""" return len(ir.ops) def remove_dead(ops): """转换:删除 dead 指令""" return [op for op in ops if op != "dead"] def fuse_mul_add(ops): """转换:把紧邻的 add 与 mul 融合为 fma""" fused = [] for op in ops: if op == "add" and "mul" in fused: fused.append("fma") else: fused.append(op) return fused def main(): ir = IRBlock(["dead", "add", "mul", "add", "sub"]) manager = AnalysisManager() dce = CompilerPass("消除死代码", [], [], remove_dead) fuse = CompilerPass("乘加融合", ["消除死代码"], ["op_count"], fuse_mul_add) check = CompilerPass("效果检查", ["乘加融合"], ["op_count"], None) report = CompilerPass("结果报告", ["效果检查"], ["op_count"], None) pipeline = topological_order([fuse, dce, check, report]) print("调度顺序:", " -> ".join(p.name for p in pipeline)) for p in pipeline: for ana in p.needs: manager.obtain(ana, ir, count_ops) if p.transform_fn is not None: ir.mutate(p.transform_fn) print("执行", p.name, "->", ir.ops, "版本号", ir.version) else: print("执行", p.name, "(只读分析 Pass,不修改 IR)") if __name__ == "__main__": main()
self.cache_map[name] = AnalysisResult(data, ir.version)重算完把结果按当前版本号存进台账,供后续 Pass 复用。if result is not None and result.version == ir.version:版本号一致说明食材没被改动,直接取缓存,零成本。indegree_map[p.name] = len(p.deps)统计每个 Pass 的入度,即它依赖的前置师傅数量。ready = [p for p in pass_list if indegree_map[p.name] == 0]没有前置依赖的 Pass 先进就绪队列,像洗菜师傅最先开工。for next_name in dependents_map.get(current.name, []):当前 Pass 完工后通知所有等它的下游 Pass。if indegree_map[next_name] == 0: ready.append(pass_by_name[next_name])下游的依赖全部解除后,它就解锁入队。raise RuntimeError("存在循环依赖,无法调度 Pass")队列空了还有 Pass 没执行,说明存在 A 等 B、B 等 A 的环。if p.transform_fn is not None:只读分析 Pass 不改 IR,所以不触发版本号增长,缓存得以保留。
常见误区
误区“每个 Pass 都是局部的小优化,先跑哪个后跑哪个应该没区别,反正最后都是优化代码,结果差不多。”
正解由于显式依赖(分析结果消费)和隐式依赖(IR 结构顺序)的存在,Pass 顺序直接决定正确性和优化效果。例如算子融合必须等死代码消除完成后、拿到准确的 op 统计,否则可能融合出错或错过优化机会。后厨里先炒后腌和先腌后炒,端出来的是两道完全不同的菜。
为何常见:日常写代码时“局部变换可交换”的直觉被错误迁移到了编译器 Pass 上,忽略了 Pass 之间有信息传递和结构前提。
误区“分析结果算一次就够了,IR 反正一直在那儿,后面的 Pass 直接复用就行,不用管什么版本号。”
正解IR 一旦被转换 Pass 修改,旧分析结果的前提就可能被破坏。MLIR 要求 Pass 通过 preservedAnalyses 声明保留范围,其余分析一律失效并按需重算。就像切碎的肉不能再按“整块肉”的信息去腌制。
为何常见:忽略了 IR 是“被逐步改写”的活食材,而不是一成不变的死数据,把缓存错误地当成了永久存储。
误区“把所有可能的依赖都写上去,调度器就不会排错顺序,管线肯定更稳,多声明总没错。”
正解过度声明会让调度器过度保守,破坏分析保留,导致缓存频繁失效、性能下降,甚至编译时间成倍增长。应声明最小且真实的依赖集——需求单写得太满,后厨反而每道菜都要重新备料。
为何常见:把“安全”错误等同于“加上更多约束”,忽略了约束带来的调度与失效成本。
误区“编译器报循环依赖,把其中一个 Pass 从管线里删掉不就好了?反正少一个优化影响不大。”
正解循环依赖通常是 Pass 职责划分不当的信号:两个 Pass 互相需要对方的产物。正确做法是抽取公共分析,让双方都只依赖该分析,从而打破环。删 Pass 只是掩盖问题,下次加回功能还会复发。
为何常见:面对报错本能想“绕过去”,而没有把它当作一次设计层面的反馈信号。
误区“一个 Pass 只做一件事是优雅设计,所以拆成很多小 Pass,组合起来性能也不会差。”
正解每个 Pass 都是一次完整的 IR 遍历加簿记,拆太碎会导致重复遍历、分析频繁失效。MLIR 实践中常把相邻小 Pass 合并为“桶”(pipeline bucket)来摊销开销。后厨里切葱和切姜可以同一个师傅顺手完成,没必要换三个人。
为何常见:把软件工程里的“单一职责”机械套用到编译管线上,忽视了编译器的遍历成本和依赖失效成本。
面试题
Q1. MLIR 中负责把一系列 Pass 注册成管线并控制执行顺序的组件是?
- A.分析管理器 AnalysisManager
- B.通行证管理器 PassManager← 正确答案
- C.方言注册表 DialectRegistry
- D.符号表 SymbolTable 管理
答案:B。PassManager 是总厨,负责注册、排序和执行 Pass;AnalysisManager 只负责分析结果的缓存与失效,相当于台账保管人。DialectRegistry 管方言注册,SymbolTable 管符号查找,与执行调度无关。
Q2. Pass A 在代码中声明自己需要 XX 分析,该分析由 Pass B 产出,这属于什么依赖?
- A.显式依赖的关系← 正确答案
- B.隐式依赖的关系
- C.硬件依赖的关系
- D.数据流依赖关系
答案:A。显式依赖 = 通过分析接口实名声明“我需要 XX”,调度器能直接读到;隐式依赖则藏在 IR 结构中,不写出来。A 的表述完全符合显式依赖的定义。
Q3. 某转换 Pass 修改了 IR 结构,但忘记更新 AnalysisManager 缓存,后果是什么?
- A.编译器直接报错退出
- B.后续 Pass 读到过期的分析← 正确答案
- C.分析结果自动在后台刷新
- D.IR 版本号自动回退
答案:B。AnalysisManager 靠版本号判断缓存是否新鲜,Pass 修改 IR 时版本号会自动推进,使旧缓存失效。如果 Pass 声明“保留了分析”却实际破坏了前提,下游就会用到过期分析,产生“用了馊肉做菜”的错误优化。
Q4. Kahn 算法进行 Pass 拓扑排序时,每一轮应优先选择哪种 Pass?
- A.入度为零的 Pass← 正确答案
- B.依赖最多的 Pass
- C.运行最快的 Pass
- D.名称排序靠前的 Pass
答案:A。入度为零 = 没有前置依赖 = 可以立即开工,对应后厨里“什么都不等”的洗菜师傅。依赖最多的 Pass 恰恰要最后才可能解锁;运行速度和名称与依赖关系无关。
Q5. 下列哪些手段可以减少分析结果的重复计算?
- A.诚实声明保留的分析← 正确答案
- B.合并相邻的小型 Pass← 正确答案
- C.每次请求都强制重算
- D.利用版本号判断缓存有效性← 正确答案
答案:ABD。诚实声明保留分析让更多缓存保鲜;合并 Pass 减少遍历和簿记;按版本号命中缓存是 AnalysisManager 的基本机制。C 是反面做法,强制重算等于每道菜都重新备料,性能必然恶化。
Q6. 当 Pass 依赖图中出现环时,PassManager 会如何表现?
- A.随机选择一个 Pass 先执行
- B.忽略该环并继续构建管线
- C.报错并拒绝构建该管线← 正确答案
- D.降低环上 Pass 的执行优先级
答案:C。环意味着没有任何 Pass 可以先开工,拓扑排序必然失败。PassManager 会直接报错终止,而不是猜一个顺序——猜顺序等于让两位师傅互相等对方,后厨永远开不了火。
Q7. 关于隐式依赖,下列说法正确的是?
- A.不通过分析接口显式表达出来← 正确答案
- B.由 IR 结构或方言顺序暗中约定← 正确答案
- C.调度器无法静态感知的部分依赖← 正确答案
- D.只会出现在 GPU 编译管线的场景
答案:ABC。隐式依赖藏在 Region 嵌套、方言转换顺序等结构里,没有显式的分析接口,调度器难以静态感知。它不是 GPU 专属,CPU 管线和任意方言都可能存在。
Q8. 一个 Pass 声明保留了分析 A,实际却破坏了 A 的前提,可能发生什么?
- A.运行时抛出异常终止编译
- B.下游 Pass 使用过期分析致错← 正确答案
- C.编译器自动重新计算该分析
- D.PassManager 拒绝执行该 Pass
答案:B。保留声明是 Pass 对调度器的承诺。一旦撒谎,AnalysisManager 会信任声明而保留缓存,下游 Pass 拿到的就是过期分析。这类错误通常不会当场报错,而是悄悄产生错误优化,难排查。
Q9. 工程实践中正确的 Pass 依赖管理包括哪些方面?
- A.显式声明每个所需的分析← 正确答案
- B.用调试打印验证管线行为← 正确答案
- C.诚实声明分析保留范围← 正确答案
- D.尽可能扩大失效分析的范围
答案:ABC。显式声明让依赖可静态检查,调试打印验证管线行为,诚实声明保留让缓存保鲜。D 是典型反模式——扩大失效范围 = 每道菜都重做,性能灾难。
Q10. 判断依赖图是否无环的最直接依据是什么?
- A.拓扑排序覆盖了全部顶点← 正确答案
- B.依赖边的数量恰好为零
- C.所有 Pass 执行时间相等
- D.分析缓存命中率百分百
答案:A。无环等价于拓扑排序能覆盖全部顶点。边数量为零只是无环的充分不必要条件——有边也可以无环;执行时间和缓存命中率与环无关,它们反映的是性能而非图结构。