想象一家米其林餐厅的后厨。晚市高峰,客人点了十道菜,主厨必须在有限时间内完成所有菜品的备料、烹饪和摆盘。他脑海中浮现的绝不只是菜谱,更是一张精密的日程表:先让学徒把蔬菜洗净——因为切配需要干净食材;腌制牛排在等切配的间隙进行;炖汤和烤鸭可以同时开火……这就是后厨的“工序调度”。 在 MLIR 编译器世界里,同样有一位“主厨”——PassManager(Pass 管理器)。它手下的每道工序就是一个 Pass(优化过程):有的负责简化计算,有的负责消除重复代码,有的负责把中间表示翻译成目标代码。而 Pass Scheduling(Pass 调度),就是主厨脑子里那张日程表——决定每个 Pass 何时运行、依赖什么、能否并行、如何串联成流水线。调度得好,编译又快又省;调度乱了,轻则优化失效,重则整道“菜”直接崩掉。 这篇文章,我们就走进 MLIR 的“后厨”,看看 Pass 调度这门手艺是如何保证每一次编译都能出菜又快又稳的。
掌握 Pass 调度是构建现代 AI 编译器的基础能力。理解执行顺序、依赖分析、管道化与并行机制,能帮你写出高效、可扩展的 MLIR 优化流程,是通往编译器工程师的核心技能。
可视化演示
展示MLIR/TVM Pass调度三维度:Pass流水线(标准编译流水线InferType→FoldConstant→FuseOps→FoldScaleAxis→Vectorize→Unroll→Lowering共7阶段SequentialPass按序执行PassEnabled检查后逐个apply)、Pass分类(ModulePass作用于整个IRModule可跨函数改动如DCE/Inline/CSE/FunctionPass作用于单个函数粒度更细如FuseOps/Simplify按作用范围分类)、依赖与动态调度(required字段声明前置Pass框架自动补执行去重/PassEnabled运行时判定按需启用条件Pass插桩计时累计),三模式切换理解Pass调度核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+C1+'
调度基础概念
主厨开工前会写一张开工清单:先洗菜、再切配、后腌制、最后下锅。每道工序都有充分的先后理由——不洗不能切,不切无法腌,不腌味道进不去。这张清单就是后厨最朴素的“工序调度”。它回答一个核心问题:这么多活儿,到底先干哪个、后干哪个?
MLIR 中的 PassManager 也维护着这样一份清单。所谓 Pass,就是编译器对中间表示(IR)的一次遍历与变换:有的删除死代码,有的把一个操作改写成等价但更高效的多个操作。PassManager 按照注册顺序依次执行这些 Pass,就像主厨按清单逐项推进工序。每个 Pass 还可以声明自己的依赖——它需要哪些前置 Pass 先跑完、哪些分析结果已就绪,PassManager 会保证这些条件满足后才轮到它。
调度策略直接决定编译质量。举个经典例子:canonicalize(规范化)负责把 IR 整理成“标准形状”,而 CSE(公共子表达式消除)负责删除重复计算。如果 CSE 跑在 canonicalize 前面,很多本可以被消除的重复子表达式会因形状不规整而漏掉;反过来,规范化先跑一遍,重复计算暴露无遗,CSE 一抓一个准。这就是调度顺序影响优化收益的生动写照。
一张清晰的开工清单,让后厨里几十号人各司其职、互不踩脚。PassManager 的这份“Pass 清单”同样让成百上千个优化步骤各安其位,谁先谁后都清清楚楚。
- M(输入模块)输入的 IR 模块,相当于从市场拉回来的一筐待处理食材,是整个流水线的起点。
- P₁(M)(首个变换)第一个 Pass 对模块做第一次加工,就像后厨的第一道“清洗”工序,去掉表面的杂质。
- 嵌套复合(逐层传递)每个 Pass 的输出作为下一个 Pass 的输入,层层接力,前一道工序的成品是后一道工序的原料。
- Pₙ(最终结果)最后一个 Pass 产出优化后的模块,也就是整条流水线的最终成品,可以直接交给后端代码生成。
Pass管道构建
后厨从来不是只有一条动线:凉菜间流水线、热炒灶台流水线、烤箱流水线各司其职。主厨把整个后厨组织成“子流程套子流程”的结构——凉菜间内部有洗、切、拌三步,热炒区又有备料、爆炒、装盘三步。这些子流程互相嵌套,却互不干扰,这就是管道化(pipeline)的思想。
MLIR 的 OpPassManager 正是这种“局中局”的体现。它允许按 IR 操作类型(Operation)构建嵌套管道:最外层通常是 builtin.module,内层可以是 func.func、scf.for 等具体操作类型。每一层操作类型都拥有一份独立的 Pass 列表,由外层的 PassManager 统一调度。这就是所谓“嵌套调度”——Module 级 Pass 在全局执行,函数级 Pass 在进入每个函数时触发。
构建管道有多种姿势。C++ API 层面,可以用 PassManager::addNestedPass<func::FuncOp>(...) 在代码里嵌套注册;命令行层面,--pass-pipeline='builtin.module(func.func(cse,canonicalize),convert-to-llvm)' 这样的文本语法让开发者不改代码就能快速拼装管道。管道构建工具链还支持重复执行控制:同一个 Pass 可以在不同层级出现多次,只要它们各自服务不同的优化目标。
凉菜间的工序不会跑到热炒区去,MLIR 的嵌套管道同样把不同层级的优化关进各自的“操作间”,让复杂编译流程变得有层次、可维护。
- OpPassManager(操作类型管理器)MLIR 为每种 IR 操作类型(如 module、func)配备的独立调度器,决定这一层管道的执行规则。
- op(操作类型)指定这层管道管辖的 IR 操作类型,它决定了管道嵌套在 IR 树的哪个位置。
- 角括号序列(有序清单)表示该操作类型内部从左到右依次执行的 Pass 列表,顺序即执行顺序。
- 管道归属(每个元素的意义)清单中的每个 Pass 都只作用于当前操作类型内部的 IR,形成清晰的职责边界。
分析结果管理
后厨的白板上记着一行行“食材状态”:鸡胸肉已腌制、西兰花已焯水、高汤已吊好。每道工序完成后打个勾,备料的厨师看一眼白板就能复用,不必反复确认。但如果有人把西兰花挪去摆盘了,那么“西兰花已焯水”这条记录就必须当场擦掉——否则后来的厨师会拿它做菜,坏了整桌宴席。
MLIR 的 AnalysisManager 就是这块白板。分析(Analysis)是编译器对 IR 的“观察结论”,比如支配关系(谁支配谁)、到达定义(变量在哪里被赋值到了这里)。AnalysisManager 把计算结果缓存起来,供后续 Pass 复用,避免重复计算。每个 Pass 执行完,必须声明自己保留(preserve)了哪些分析;其余分析默认全部失效,下次需要时重新计算。
这里有一个关键的“保守失效策略”:Pass 无法精确知道自己的改动影响了哪些分析,所以凡是没声明保留的,一律当作失效处理。这不是偷懒,而是安全底线——保留一个过期的分析结果,可能导致后续 Pass 基于错误信息做优化,最终生成有 bug 的代码。失效粒度控制得越精确,缓存的复用率就越高,编译就越快。
白板只记录仍然为真的状态,谁动了食材,谁就要负责更新记录。AnalysisManager 用同样的纪律管理分析缓存,让每一次复用都建立在可靠的信息之上。
- A(分析结果全集)缓存中所有分析结果的集合,比如支配关系、活跃变量、别名信息等。
- uses(a)(依赖的数据)分析 a 在计算时读取的 IR 信息集合,相当于它的“原料清单”。
- writes(Pass)(修改的数据)Pass 执行后实际改动过的 IR 信息集合,相当于“动了哪些食材”。
- 交集为空(保留条件)两者不相交意味着分析的原料没被动过,结果继续有效;反之则必须删除。
并行调度执行
晚市高峰只有一台灶台肯定不够。主厨把互不依赖的菜分给不同厨师:小张负责炖汤,小李负责炒菜,小王负责凉菜,各自守着自己的灶台和食材,互不抢铲子。只要工序之间没有依赖,就可以同时开工。这就是并行调度最朴素的直觉。
MLIR 的 PassManager 支持同样的并行策略。开启多线程(enableThreading())后,函数级 Pass 可以通过 llvm::parallelFor 并行作用于多个函数——每个函数就像一位独立厨师,在同一时间处理各自的 IR 副本。但前提是严格隔离:函数级 Pass 必须保证线程安全,不能读写共享的全局状态,不能修改 Module 级别的结构。Module 级 Pass 依然串行执行,它们是并行区域的“栅栏”。
并行调度的收益用加速比衡量。理想情况下,N 个函数并行处理,耗时可缩小到接近原来的 1/N。但现实中线程调度、分析缓存隔离、内存带宽竞争都会带来额外开销,所以加速比往往低于线程数。这也是为什么调度器需要精细控制并行粒度:函数太小,并行开销甚至可能超过收益;函数足够大,并行才有意义。
并行不是让厨师们互抢铲子,而是把互不相干的菜分给不同灶台。PassManager 用线程安全和结果隔离划清“灶台边界”,让并行优化既快又稳。
- T_serial(串行耗时)不开启多线程时,一个函数接一个函数依次处理的总耗时。
- T_parallel(并行耗时)多线程下多个函数同时处理所需的墙钟时间(wall-clock time)。
- 比值 S(加速比)串行与并行耗时的比值,是衡量并行调度性能提升的核心指标。
诊断与验证
主厨在后厨装了一整套监控:每个灶台有计时器,每道工序有记录本,天花板上还有摄像头。一旦某道菜出了问题,回放录像就能精确找出是哪个环节、哪个动作出了纰漏。没有这套监控,主厨只能拍着脑袋猜是哪一步做坏了。
MLIR 为 Pass 调度提供了同样全面的“后厨监控系统”。--mlir-timing 给每个 Pass 计时,输出耗时排行榜,让“哪个 Pass 拖慢了编译”一目了然;--mlir-pass-statistics 输出 Pass 内部的统计信息,比如 CSE 消除了多少个冗余表达式;--mlir-print-ir-before/after-all 在指定 Pass 前后打印 IR,方便逐级比对变换效果;此外还有 --mlir-pass-pipeline 可以打印当前调度管道,帮助开发者理解编译流程。
其中最亮眼的工具是崩溃重放器(Crash Reproducer):当某次编译在某个 Pass 中崩溃时,它会自动记录到崩溃那一刻为止的 Pass 序列,并生成一段 --pass-pipeline 文本。开发者把这段文本粘回命令行,就能在极小规模下复现崩溃现场,定位问题又快又准。日志还能分级控制输出量,避免调试大型模型时被海量 IR 打印刷屏。
有了监控,主厨不再“觉得”某个环节慢,而是精确知道它慢了零点几秒。Pass 调度的优化工程也因此从凭经验拍脑袋,进化成了用数据说话的精密管理。
- T(Pᵢ)(单个 Pass 耗时)第 i 个 Pass 从进入执行到结束返回的时间,是计时排行榜上的基本单位。
- 求和(累加全部 Pass)把所有 Pass 的耗时累加,得到优化阶段花费的总时间。
- T_overhead(调度开销)PassManager 自身的管理成本,包括依赖检查、分析失效簿记、IR 验证等。
调度实用策略
老主厨的实战心法往往写在最不起眼的地方:肉要提前解冻、高汤要最早点火、凉菜要最后摆盘……这些先后次序不是菜谱上写死的,而是无数次实战总结出的最优解。调度实用策略,就是把这些心法提炼成可操作的原则。
第一条心法:规范化前置。canonicalize 应该在大多数 Pass 之前运行,它把 IR 整理成一致的标准形状,让后续 Pass 面对“熟悉的食材”,匹配模式更容易命中。第二条心法:内联后紧跟 CSE。内联(inline)会引入大量重复代码,必须立刻用 CSE 清理战场,否则冗余代码会像客人走后没收拾的餐桌一样堆积。第三条心法:用文本管道做实验。--pass-pipeline 命令行语法让开发者不重新编译就能尝试不同调度组合,配合 --mlir-timing 和 --mlir-pass-statistics 数据,快速找到性价比最高的调度方案。
还要警惕“多多益善”的诱惑。同一个 Pass 在管道里重复插入不一定带来更多收益,反而可能造成 IR 在两个 Pass 之间反复变形、永不收敛。判断一个 Pass 是否值得保留,标准只有一条:它带来的优化收益是否大于它的编译代价。数据驱动的调优,才是保证编译质量与编译速度双赢的正道。
心法不是菜谱背得多,而是知道每道工序的代价与收益。Pass 调度的最高境界,就是像老主厨一样,在有限时间内把每道菜都安排在它最出彩的位置上。
- σ(调度方案)一种 Pass 排列顺序,比如“先规范化、再 CSE、后转换到 LLVM”。
- Benefit(σ)(总收益)该顺序下所有 Pass 带来的优化收益总和,如消除的冗余指令数、化简的运算量。
- Cost(σ)(总代价)该顺序下所有 Pass 的编译耗时总和,以及潜在的变换风险成本。
- argmax(取最大值)在所有可能的调度方案中,挑选“收益减代价”最大的那一个,称为最优调度。
AI 应用场景
大模型推理引擎的优化流水线
阶段:模型编译与图优化
解决的问题:把 TensorFlow/PyTorch 导出的计算图转换成高效的推理算子序列,需要经历几十个优化步骤。手工编排这些步骤容易遗漏依赖,也难以复用分析结果;Pass 调度让编译器自动保证依赖与顺序,优化组合可以像积木一样模块化插拔。
具体案例:IREE、TensorFlow Lite 这类编译器会把模型图逐级 Lower 到 TFLite、再到 Vulkan/SPIR-V 后端。工程师用 --pass-pipeline 文本描述整条管道,在 CI 中自动验证每个调度组合的正确性与端到端性能,找到“规范化→量化→算子融合→代码生成”的最优顺序。这就像主厨把后厨动线写成标准作业流程,新菜品只需改几个环节就能复用整条流水线。
端侧 NPU 编译器的并行编译加速
阶段:部署构建与编译性能调优
解决的问题:手机端 NPU 模型导出时,数十个算子函数需要逐一做内存规划和寄存器分配,串行编译可能要耗时数分钟。启用函数级 Pass 并行调度后,每个函数独立优化、互不干扰,编译耗时大幅缩短,直接缩短了模型迭代上线的等待时间。
具体案例:某端侧推理引擎对 128 个算子对应的函数逐个执行 buffer 规划 Pass,开启 --mlir-enable-threading 后,编译时间从 40 秒降到 12 秒,加速比约 3.3 倍。工程师还通过 --mlir-timing 定位到最耗时的 Pass,进一步对单个 Pass 做了算法优化。这就像给后厨增派人手,把 10 道菜分给 4 位厨师同时做,出菜速度自然更快。
自定义算子库的 Pass 编排
阶段:算子开发与优化
解决的问题:为 AI 芯片开发自定义算子时,开发者往往需要把自己的优化 Pass 插入到已有调度管道中,同时不能破坏原有分析和后续 Pass 的假设。Pass 调度器通过依赖声明机制,让自定义 Pass 与内置 Pass 无缝协作。
具体案例:某 GPU 算子库团队开发了一个 GemmRewrite Pass,把通用矩阵乘法改写为硬件专用的分块 Kernel。他们声明该 Pass 依赖支配关系分析,并把它安排在 canonicalize 与向量化之间,通过 --mlir-print-ir-after-all 逐级验证变换正确性,最终在不改动其他 Pass 的前提下将目标算子性能提升了 2.4 倍。这就像一位新来的甜品师,只需跟主厨说清自己需要什么工具和原料,就能无缝融入后厨的既有流程。
Python 演示
import timefrom dataclasses import dataclass, fieldfrom typing import Dict, List, Set @dataclassclass PassNode: """一个 Pass 节点:名称 + 依赖的前置 Pass 名""" name: str deps: Set[str] = field(default_factory=set) class SimplePassManager: """迷你 Pass 调度器:展示依赖排序、串行执行与计时""" def __init__(self): self.pass_nodes: List[PassNode] = [] self.executed: List[str] = [] self.timings: Dict[str, float] = {} def add_pass(self, name: str, deps: Set[str] = None) -> "SimplePassManager": self.pass_nodes.append(PassNode(name=name, deps=deps or set())) return self def _find(self, name: str) -> PassNode: return next(p for p in self.pass_nodes if p.name == name) def _schedule(self) -> List[str]: """DFS 拓扑排序:保证依赖 Pass 永远排在前面""" order: List[str] = [] visited: Set[str] = set() def dfs(node: PassNode) -> None: if node.name in visited: return visited.add(node.name) for dep in node.deps: dfs(self._find(dep)) order.append(node.name) for node in self.pass_nodes: dfs(node) return order def run(self, ir: Dict) -> Dict: plan = self._schedule() print("调度顺序:", " -> ".join(plan)) for name in plan: t0 = time.perf_counter() ir["version"] = ir.get("version", 0) + 1 ir["last_pass"] = name dt = time.perf_counter() - t0 self.timings[name] = dt self.executed.append(name) print(f" 执行 {name:12s} IR 版本={ir['version']} 耗时={dt*1000:.2f}ms") return ir if __name__ == "__main__": pm = SimplePassManager() pm.add_pass("dom", deps=set()) # 支配分析 pm.add_pass("cse", deps={"dom"}) # CSE 依赖支配分析 pm.add_pass("canonicalize", deps={"dom"}) # 规范化依赖支配分析 pm.add_pass("convert-llvm", deps={"canonicalize"}) ir = {"version": 0} pm.run(ir) print("总耗时: %.3fms" % (sum(pm.timings.values()) * 1000))
pm.add_pass("dom", deps=set())注册一个无依赖的分析 Pass,就像后厨第一批开工的“备料”工序。pm.add_pass("cse", deps={"dom"})声明 CSE 依赖 DOM 分析,调度器会保证 DOM 永远先执行。plan = self._schedule()用 DFS 拓扑排序得到合法的执行顺序,保证“先洗后切”。for dep in node.deps: dfs(self._find(dep))递归先完成所有前置依赖,这是拓扑排序的核心。ir["version"] = ir.get("version", 0) + 1模拟 Pass 修改 IR,每个 Pass 都会推进 IR 版本号。self.timings[name] = dt记录每个 Pass 的耗时,对应 MLIR 的--mlir-timing功能。
常见误区
误区Pass 都是独立的优化,排在前在后不都一样吗?反正每个 Pass 都能把代码优化一遍,最后结果应该差不多。
正解顺序对优化效果影响巨大。比如 CSE 依赖公共子表达式“长成什么样子”被规范化整理过,内联产生的重复代码必须紧跟 CSE 清场。PassManager 只保证依赖满足,不保证“这样排收益最大”;设计调度顺序本身就是性能工程师的核心工作,就像主厨不会把炒菜安排在洗菜前面一样。
为何常见:初学时每个 Pass 看起来都是“一个优化动作”,容易忽略 Pass 之间的数据流联系和收益叠加效应。
误区打开了多线程,Pass 就会自动安全地并行跑,我什么都不用管。
正解函数级 Pass 并行要求严格的线程安全:Pass 内部不能修改共享的 PassManager 状态、不能依赖全局静态变量。MLIR 的 Module 级 Pass 依然串行执行,像一道“栅栏”横在并行区域之间。如果不隔离好共享状态,数据竞争会让程序得到不确定的结果——这在编译器里是最危险的 bug。
为何常见:“并行”在直觉上是“自动加速”,人们容易忽略并发编程的经典陷阱:共享可变状态。
误区支配关系这类分析是客观事实,算出来以后永远不会变,直接用就行。
正解分析结果只在它所依赖的 IR 信息未被修改时有效。Pass 修改 IR 后,凡是未声明保留的分析全部默认失效。保留一个过期分析,等于拿着昨天的账本去买今天的菜,可能让后续 Pass 基于错误信息生成坏代码。所以失效策略宁可保守:拿不准就重算。
为何常见:人们把“分析”当成固定不变的常量,忽略了 IR 在 Pass 流水线上是不断被改写的新鲜食材。
误区优化是做加法,多插几个 Pass 总能多消除一些冗余,编译质量肯定更高。
正解每个 Pass 都有编译耗时和潜在风险。盲目重复插入 Pass 可能导致 IR 在两个变换之间反复震荡——canonicalize 改成 A 形状,下一个 Pass 又改回 B 形状,甚至永远不收敛。正确的做法是用 --mlir-timing 和 --mlir-pass-statistics 量化每个 Pass 的代价与收益,只保留净收益为正的 Pass。
为何常见:“多多益善”是新手最自然的直觉,但优化不是加法,而是成本与收益的权衡。
误区`--mlir-timing` 输出的表格里,所有 Pass 的耗时加起来不就是总编译时间吗?
正解总编译时间 = Pass 执行时间 + 调度器开销 + 分析失效重建 + IR 验证与簿记成本。Pass 计时只展示 Pass 自身的耗时,PassManager 的管理成本、Pass 之间分析和 IR 传递的开销并没有计入。当 Pass 数量庞大时,这些“隐含开销”可能占总时间的 10% 以上,忽略它们会导致对编译性能的误判。
为何常见:工具默认只输出 Pass 列表,人们容易把这张表当成完整账本,忘了管理成本也是成本。
面试题
Q1. MLIR 中 PassManager 的核心职责是?
- A.自动生成优化代码
- B.确定调度执行顺序← 正确答案
- C.解析源代码语法
- D.管理编译时内存
答案:B。PassManager 的核心职责是管理 Pass 的注册、依赖解析和执行顺序,就像主厨安排后厨工序。A 是 Pass 本身做的事,C 是前端解析器的事,D 不是 PassManager 的职责。
Q2. Pass 依赖关系的核心作用是?
- A.保证前置步骤已完成← 正确答案
- B.让编译速度更快
- C.使代码体积更小
- D.支持语言特性扩展
答案:A。Pass 声明依赖是为了让 PassManager 保证前置 Pass 或分析结果先就绪,相当于“先洗菜再切菜”的工序约束。B、C 是优化目标而非依赖关系的作用,D 与依赖无关。
Q3. OpPassManager 嵌套调度的依据是?
- A.按 IR 操作类型区分← 正确答案
- B.按源码行号排序
- C.按 Pass 名称排序
- D.按文件大小划分
答案:A。OpPassManager 按 IR 操作类型(如 module、func.func)构建嵌套管道,每种操作类型拥有自己独立的 Pass 列表。这就像后厨按凉菜间、热炒间分区管理,而不是按行号或名称排顺序。
Q4. 分析结果“失效”的正确含义是?
- A.分析代码运行出错
- B.数据过期重新计算← 正确答案
- C.分析结果保存更久
- D.分析改为并行执行
答案:B。失效指的是缓存的分析结果因为 IR 被修改而不再有效,需要删除并在下次需要时重建。就像白板上“西兰花已焯水”的记录,在西兰花被挪作他用后必须擦掉,而不是出错或保存更久。
Q5. 下列属于 Pass 诊断手段的有?
- A.编译计时统计← 正确答案
- B.Pass 统计信息
- C.崩溃重放机制
- D.调试断点设置
- E.各 Pass 间无数据竞争
- F.各 Pass 需按字典序排序
- G.各 Pass 必须共享状态
- H.各 Pass 名称必须唯一
答案:A。并行调度要求函数级 Pass 线程安全,即不同函数上的 Pass 实例之间没有共享可变状态,否则会产生数据竞争。排序、名称唯一、强制共享状态都不是并行的前提,反而共享状态正是要避免的。
Q6. 关于分析结果缓存的正确理解是?
- A.首次计算后可复用← 正确答案
- B.一旦失效立即删除← 正确答案
- C.所有分析永不失效
- D.失效后按需重算← 正确答案
答案:ABD。分析结果首次计算后被 AnalysisManager 缓存,供后续 Pass 复用;一旦失效立即删除;下次 getAnalysis<T>() 时按需重算。C 错误——IR 被修改后分析必然可能失效,永远不失效的假设会导致错误优化。
Q7. 保守失效策略的主要原因是什么?
- A.无法确定分析受影响← 正确答案
- B.为了减少内存占用
- C.为了提高分析精度
- D.为了加快编译速度
答案:A。Pass 通常无法精确知道自己的变换影响了哪些分析,所以凡未声明保留的一律失效,宁可重算也不冒险。这是安全优先的策略。它不会减少内存占用,也不是为了提高精度或加快速度,反而可能因重算变慢,但换来了正确性。
Q8. 声明 Pass 依赖的正确做法是?
- A.声明前置分析依赖← 正确答案
- B.直接执行不声明
- C.手动复制分析代码
- D.忽略所有依赖关系
答案:A。Pass 应通过依赖声明机制告诉 PassManager 自己需要哪些前置分析或 Pass,调度器会自动保证它们先完成。不声明、复制代码、忽略依赖都会破坏调度的正确性,就像不说清楚需要的食材,主厨没法帮你备料。
Q9. 提升 Pass 调度性能的合法手段有?
- A.并行无依赖函数 Pass← 正确答案
- B.复用有效分析缓存← 正确答案
- C.重复插入同一 Pass
- D.用计时定位慢 Pass← 正确答案
答案:ABD。并行执行无依赖的函数级 Pass 能利用多核加速;复用有效的分析缓存避免重复计算;用 --mlir-timing 定位耗时 Pass 再做针对性优化,都是合法手段。C 盲目重复插入同一 Pass 可能导致 IR 震荡不收敛,反而降低性能,需要谨慎评估。