阿安入职了一家做AI编译器的小公司。第一天,团队Leader没给他分配复杂的任务,而是讲了一个比喻:“我们的编译器就像一个长途旅行者,中间表示(IR)就是他的行李箱。每一轮优化Pass,就是一次整理行囊的动作。而你,负责确保行李箱里每样东西都会被真正用到。” 阿安接到的第一个任务,就是理解一个叫 DCE(Dead Code Elimination,死代码消除)的 Pass。它像是那个告诉你“这件破外套永远不会再穿、扔掉吧”的打包顾问。想象你搬家时面对满屋杂物:有些东西确实有用,必须留;有些东西虽然没坏,但它的计算结果——啊不,它的“用途”——永远不会被任何人看见,那它就是死代码。DCE 要做的,就是借助一张“依赖关系网”,在保证不碰坏任何必需物品的前提下,把死代码从行李箱里清出去。本文就用这个“搬家打包”的故事,带你完全看懂这个 AI 编译器里的基础优化。

岗位关联

掌握 DCE,你就能看懂现代 AI 编译器中 Pass 流水线的骨架逻辑;这是每一位 AI 编译器工程师理解“优化从哪来、冗余去哪了”的必修课。

目录
  1. 可视化演示
  2. 核心目标:扔掉永远不会用到的“破外套”
  3. 依赖分析基础:弄清行李箱里的“连带关系”
  4. 工作流程:三步打包法
  5. MLIR实现机制:打包App的运行原理
  6. 流水线协同:和其他 Pass 打配合
  7. 价值与局限:行李箱变轻了,边界也清晰了
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示DCE的活性分析核心机制:从输出反向传播活跃集合,识别并删除无副作用、结果未被使用的死代码,缩小IR体积。

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

活性分析:反向传播 从输出反向标记活跃定义 未活跃且无副作用则删除 总结:DCE经活性分析删除无用计算。

核心目标:扔掉永远不会用到的“破外套”

阿安打开源码里生成的中间表示(IR),仿佛打开一个塞满东西的行李箱。里面有一堆“看起来很努力”的计算操作:有的算乘法,有的算 tanh,有的算 log。但 Leader 告诉他:“里面有些东西,算了也白算——因为它的结果根本不会影响模型的最终输出。”

这些“算了也白算”的操作,就是 死代码。DCE(Dead Code Elimination)的目标,就是把它们从行李箱里挑出来扔掉。判断标准有两条:第一,这个操作 没有副作用——它不打印、不写内存、不改变任何外部可见的状态,就像一件躺在箱底的旧外套不碰不响;第二,它的 结果没有任何人使用——没有后续操作消费它的输出,就像这件外套永远不会再被穿上。两个条件同时满足,编译器才能安全删除它。

安全,是 DCE 的底线。传统的“手动删代码”靠人的直觉,而 DCE 靠严格的证明:只有证明了某个操作既不起作用、又没副作用,才会动手。这样一来,IR 的体积减小了、后端代码生成的负担减轻了,而程序的语义(模型最终算出的结果)分毫不变。

阿安恍然大悟:“原来扔掉一件东西,不是因为'我觉得没用',而是因为它确实不被需要,也碰不到任何其他东西。”

Removable(op)⟺¬SideEffect(op)∧use(result(op))=0\displaystyle \text{Removable}(op) \Longleftrightarrow \neg\text{SideEffect}(op) \wedge \text{use}(\text{result}(op)) = 0
这个公式是 DCE 的“判决书”——只有“没有副作用”和“结果无人用”两行同时盖章,一个操作才被判为死代码。
  1. 主对象 op指 IR 中的一条计算指令,比如一次 arith.mulf 浮点乘法,它是 DCE 判决的最小单位。
    op\text{op}
  2. 副作用判定 SideEffect(op)检查 op 除了输出结果之外,是否还会对外部世界产生可见影响,比如打印信息、写入全局内存。
    ¬SideEffect(op)\neg \text{SideEffect}(op)
  3. 零消费判定 use(result(op)) = 0统计 op 的结果被其他操作引用的次数。若为 0,说明这个计算结果永远不会被消费。
    use(result(op))=0\text{use}(\text{result}(op)) = 0
  4. 等价推导 Removable两个条件同时成立时,编译器就能安全地移除 op,而不改变程序行为。
    Removable(op)⟺¬SideEffect(op)∧use(result(op))=0\text{Removable}(op) \Longleftrightarrow \neg\text{SideEffect}(op) \wedge \text{use}(\text{result}(op)) = 0

依赖分析基础:弄清行李箱里的“连带关系”

光知道“没人用的东西该扔”还不够。阿安很快发现,有些物品之间藏着连带关系:帐篷和帐篷支架是一对,带上帐篷就必须带支架;露营灯依赖电池,没电池灯就是废铁。行李箱里的每件物品,都被一张看不见的“依赖网”拴着。

编译器里这张网叫 def-use 链。一个 op 会 “def“(定义)一个值,其他操作则 ”use“(使用)这个值作为输入。DCE 要做的事,就是顺着这张网反向摸清”谁依赖谁”。举例来说,如果操作 C 使用操作 A 的输出作为输入,那么当 C 被保留时,A 通常也得保留——因为 C 的运行少不了 A 喂过来的数据。

同时还要警惕一类特殊物品:有副作用的操作。它们就像辣椒水喷雾——虽然平时没人“用它”(结果无人消费),但只要它存在,就可能在某个时刻对外界喷一下(打印、写内存、触发随机事件)。这类操作即使在依赖网里“没有下家”,DCE 也无权扔掉它,因为删掉它可能会改变程序对外部世界的可见行为。

基于这张网,编译器给每个值定义一个 活跃性:一个值只要还有任何一个“被标记保留”的操作在消费它,它就是活跃的(活的);反过来,若它的所有潜在消费者都死了,它就跟着死掉。

阿安在笔记本上画下这张依赖网,感叹道:“原来判断一件行李该不该留,得先问清楚:还有谁在用它?”

Active(v)≜∃ u∈users(v):Marked(u)\displaystyle \text{Active}(v) \triangleq \exists\, u \in \text{users}(v): \text{Marked}(u)
活跃性是 DCE 的“生命力探测仪”:一个值还活着,当且仅当存在一个已被标记保留的操作在消费它。
  1. 值 v某个操作产出的数据,相当于行李箱里的一件物品——比如帐篷、电池或一件旧外套。
    vv
  2. 使用者集合 users(v)所有把 v 当作输入消费的操作集合,也就是“谁会用到这件物品”。
    users(v)\text{users}(v)
  3. 存在量词 ∃只要使用者集合里有一个操作 u 已经被标记保留(Marked(u) 为真),这个值就算活着。
    ∃ u∈users(v)\exists\, u \in \text{users}(v)
  4. 活跃性定义 Active(v)v 活着意味着它至少支撑着一个被保留的操作,因此不能被随便删除。
    Active(v)≜∃ u∈users(v):Marked(u)\text{Active}(v) \triangleq \exists\, u \in \text{users}(v): \text{Marked}(u)

工作流程:三步打包法

有了依赖分析这张“物品关系图”,阿安总结出了 DCE 的 三步打包法。这是整个优化最核心的操作流程。

第一步,收集根集合。根是“无论如何都必须保留”的操作,包括两类:一是带有副作用的操作(如打印、写内存),二是输出被外部世界消费的操作(如函数的返回结果)。它们就像旅行前确定的“必带清单”——清单上的东西一样都不能少。

第二步,反向标记传播。从根集合出发,沿着 def-use 链的反方向(从消费者回溯到生产者)逐步扩散:凡是被标记操作依赖的生产者,一律打上“保留”的标记。这个过程就像从必带清单倒推——既然要带帐篷,那帐篷支架和地钉也得进清单。

第三步,删除未标记操作。遍历完整张图后,把所有没有被标记的操作收集起来,统一从 IR 中抹去。由于所有有副作用的操作一开始就在根集合里,所以能走到这一步的操作,必然是无副作用的“纯计算”,删除它们绝对安全无事故。

阿安把三步打包法写在了工位上——“先定必带清单,再反推依赖,剩下的全扔掉”。这个流程清晰、安全、可证明。

Marked(o):=Root(o) ∨⋁u∈Users(o)Marked(u)\displaystyle \text{Marked}(o) := \text{Root}(o) \ \vee \bigvee_{u \in \text{Users}(o)} \text{Marked}(u)
这个公式是 DCE 引擎的“点火开关”:一个操作要么本身是根,要么被至少一个已标记的消费者所依赖,否则它不配留在行李箱里。
  1. 根操作 Root(o)o 本身有副作用,或者是程序的对外输出。根操作是整个标记过程的起点。
    Root(o)\text{Root}(o)
  2. 使用者集合 Users(o)所有消费 o 输出的操作。在反向传播中,这些使用者是“下游”的裁判。
    Users(o)\text{Users}(o)
  3. 逻辑或 ∨只要有一个使用者被标记,o 就会因为被需要而获得标记。
    ⋁u∈Users(o)Marked(u)\bigvee_{u \in \text{Users}(o)} \text{Marked}(u)
  4. 赋值传播=:该规则不断迭代,直到没有新的操作被标记,此时标记集合达到不动点。
    Marked(o):=Root(o)∨⋁u∈Users(o)Marked(u)\text{Marked}(o) := \text{Root}(o) \vee \bigvee_{u \in \text{Users}(o)} \text{Marked}(u)

MLIR实现机制:打包App的运行原理

阿安开始读 MLIR 的源码,想看看 DCE 在真实系统里长什么样。他发现,在 MLIR 的世界里,DCE 被封装成了一个 标准 Pass。所谓 Pass,就是一个独立可复用的优化模块,有点像手机里的“打包 App”——用户(编译器工程师)不需要自己动手翻行李箱,只要按下按钮,App 就会自动执行整理流程。

要让 App 能被调用,第一步是 注册 Pass。MLIR 有一套 Pass 注册机制,开发者可以在任意位置声明并注册一个新的 DCE Pass,随后它就能被插入到优化流水线中,和其他 Pass 按顺序执行。这就像在手机应用商店上架一个新的“打包工具”,装好后随时可以启动。

真正干活时,Pass 内部要回答一个问题:“这个操作的结果到底有没有消费者?” 对应的工具是 use_empty() 查询——它直接检查某个操作的结果是否没有任何使用记录。如果返回真,说明这个操作的结果无人问津,很有可能是死代码。另一个关键能力是 遍历(walk) 。MLIR 中的 IR 以操作树或区域(Region)的形式组织,Pass 通过 walk 接口按顺序访问每一个操作,把它们逐个丢进“标记→删除”的判定流程。

阿安终于明白:MLIR 里的 DCE 不是一个神秘魔法,而是一个“注册好的、会走路、会问话的打包 App”——它走遍每个角落,问清每件物品的用途。

∀o∈Regionwalk:Erase(o)⟺¬Marked(o)∧¬SideEffect(o)\displaystyle \forall o \in \text{Region}_{\text{walk}}: \quad \text{Erase}(o) \Longleftrightarrow \neg\text{Marked}(o) \wedge \neg\text{SideEffect}(o)
这是 Pass 内部的主循环逻辑:walk 把区域内每个操作都拎出来过一遍,能删的删,该留的留。
  1. 全称量词 ∀o遍历整个 Region 中的所有操作,一个不落。
    ∀o∈Regionwalk\forall o \in \text{Region}_{\text{walk}}
  2. 删除判定 Erase(o)程序遍历到操作 o 时,决定它是否被移出 IR。
    Erase(o)\text{Erase}(o)
  3. 两重否定条件既没有被标记(¬Marked),又没有副作用(¬SideEffect),才允许删除。
    ¬Marked(o)∧¬SideEffect(o)\neg\text{Marked}(o) \wedge \neg\text{SideEffect}(o)
  4. 等价关系 ⟺遍历过程中对每个操作做出的判决,与“安全无影响”是完全等价的。

流水线协同:和其他 Pass 打配合

问题来了。阿安发现,光靠 DCE 一个 Pass,行李箱还是不够整齐。因为死代码不是凭空出现的,很多是“兄弟 Pass 制造出来的”。

最典型的搭档是 CSE(公共子表达式消除)。CSE 干的事是把两段一模一样的计算合并成一段。打个比方:行李箱里有两包同样的压缩毛巾,CSE 会把它们合成一包,省出一份空间。但合并之后,原来第二包毛巾所在的位置就空出来了——而那条“多余的计算链”还没被清理掉。这时候 DCE 就该上场了:CSE 创造冗余,DCE 清扫冗余,两兄弟配合默契。

所以 DCE 在优化流水线中通常 放在中后期。前面的 Pass(如方言转换、常量折叠、CSE)负责“制造”和“暴露”冗余,DCE 负责“收尾”。如果把它放在最前面,它只能清掉最原始的一小撮死代码,后续其他 Pass 新产生的死代码就没人管了。

另一个朴素的真理是:一次 DCE 往往不够。编译优化的过程就像反复整理房间——你整理好了,家人又翻了东西出来,得再整理一遍。实践中,DCE 经常会被执行多轮,每轮之间穿插其他优化,直到 IR 的体积达到一个稳定的低点。

阿安感叹:“原来 Pass 之间不是各干各的,而是一条配合默契的流水线。DCE 是站在最后收拾残局的清道夫。”

Wfinal=Winitial−∑i=1kΔWDCE(i)\displaystyle W_{\text{final}} = W_{\text{initial}} - \sum_{i=1}^{k} \Delta W^{(i)}_{\text{DCE}}
这个公式记录着行李箱的“减重账本”:最终重量是初始重量减去每一轮 DCE 清理掉的冗余之和。
  1. 初始体积 W_initial优化流水线开始前,IR 中的操作总数——对应行李箱刚装完的原始重量。
    WinitialW_{\text{initial}}
  2. 单轮清理量 ΔW_DCE(i)第 i 轮 DCE 删除的操作数——每次“扔掉的东西”有多少。
    ΔWDCE(i)\Delta W^{(i)}_{\text{DCE}}
  3. 求和符号 Σ把 k 轮 DCE 的清理量全部加起来,得到总减重量。
    ∑i=1kΔWDCE(i)\sum_{i=1}^{k} \Delta W^{(i)}_{\text{DCE}}
  4. 最终体积 W_final优化后的 IR 体积,是初始体积扣除总减重的结果。

价值与局限:行李箱变轻了,边界也清晰了

经过一轮轮优化,阿安终于看到了 DCE 的实在收益。行李箱从 26 公斤降到了 23 公斤:代码生成的指令更少了,后端编译器的工作量明显下降,寄存器的压力也小了很多。在大型模型编译中,哪怕只是把 3% 的死代码清掉,产出的目标代码体积和编译耗时都会有可感知的改善。

更重要的是,DCE 提升了整个编译器的可扩展性。有了 DCE 兜底,其他 Pass 可以放心大胆地生成“临时中间结果”——它们不需要担心自己制造的冗余会拖垮后端,因为 DCE 会在流水线末端把垃圾全部收走。这让每个优化模块的代码写起来更专注、更干净。

但阿安也撞上了 DCE 的边界:有副作用的操作,永远无法被 DCE 自动清除。就像那瓶安检禁止托运的辣椒水,虽然没人“用”它,但它属于危险品,不能扔。一个会打印日志、会写全局缓冲区的操作,即使它的返回值没有任何消费者,DCE 也会把它完整保留——因为删除它,可能就把“程序对外部世界说过的话”给抹掉了。

阿安在周会上分享战果:“DCE 让我明白,优化不是蛮力消除,而是深刻理解每一件物品存在的意义。该留的留,该扔的扔,边界画得清清楚楚。” 从此,他看整个优化流水线的眼光,都不一样了。

Keep(o)⟺SideEffect(o) ∨ Marked(o)\displaystyle \text{Keep}(o) \Longleftrightarrow \text{SideEffect}(o) \ \vee \ \text{Marked}(o)
这是 DCE 的“保留判决书”:一个操作要么因为自身有副作用必须留,要么因为被标记为活跃必须留,两条路至少要走一条。
  1. 保留判决 Keep(o)最终决定操作 o 是否留在 IR 中。
    Keep(o)\text{Keep}(o)
  2. 有副作用 SideEffect(o)o 可能产生外部可见影响(打印、内存写入、随机状态变化)。
    SideEffect(o)\text{SideEffect}(o)
  3. 被标记 Marked(o)o 在反向标记阶段被证明是活跃链路的一部分。
    Marked(o)\text{Marked}(o)
  4. 逻辑或 ∨满足任何一个条件,o 就获得“留队”资格;两者皆无才判死刑。

AI 应用场景

场景一:方言转换后的 IR 清扫

阶段:AI 编译器前端生成阶段(方言转换与合法性检查)

解决的问题:把 ONNX 模型导入 MLIR 并转换为 Linalg 方言时,转换脚本往往会生成大量“过渡性”的临时操作,比如仅用于形状推导的 tensor.empty 和 linalg.fill。这些操作的计算结果在后续流程中无人使用,却白白占用 IR 体积、拖慢后端代码生成。传统做法是人工审查生成代码,效率极低且容易遗漏。

具体案例:某团队开发自定义算子时,ONNX 到 Linalg 的转换引入了约 200 个仅用于中间铺陈的操作。在 Pass 流水线中插入一轮 DCE 后,IR 中操作数减少约 12%,后端编译时间下降 15%,而生成的推理结果与优化前完全一致。

场景二:量化部署前的冗余清理

阶段:模型优化与转换(量化、剪枝后的编译优化)

解决的问题:在 PTQ(训练后量化)流程中,模型原先用于动态范围校准的算子(如统计激活值 min/max 的算子)只在转换阶段需要。量化完成后,这些算子的输出已不再被任何推理操作消费,形成了典型的死代码。若不清理,它们会作为冗余调度块出现在最终推理引擎中,增加 CPU/GPU 的无效计算。

具体案例:某端侧推理引擎在导出色量化模型时,通过 MLIR 流水线先执行常量折叠,再执行 DCE,一次性清除了 30 多个校准算子及其衍生计算。最终 IR 体积减少 8%,端侧推理延迟下降约 3%,且模型精度损失为零。

Python 演示

python76 行
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""模拟 MLIR 中死代码消除(DCE) Pass 的核心逻辑."""  class Operation:    """一个极简 IR 操作节点."""     def __init__(self, name, has_side_effect=False):        self.name = name        self.inputs = []          # 生产输入的操作(生产者)        self.users = []           # 消费本操作输出的操作(使用者)        self.has_side_effect = has_side_effect     def add_use(self, user):        self.users.append(user)  def erase_dead_code(ops, roots):    """从根集合开始反向标记, 删除未标记且无副作用的操作."""    marked = set()    stack = list(roots)    while stack:        op = stack.pop()        if op in marked:            continue        marked.add(op)        stack.extend(op.inputs)     kept, removed = [], []    for op in ops:        if op in marked or op.has_side_effect:            kept.append(op)        else:            removed.append(op)    return kept, removed  def build_sample_ir():    """构造一个小型计算图: 输出链 + 一条死代码分支."""    c1 = Operation("arith.constant 1")    c2 = Operation("arith.constant 2")    c3 = Operation("arith.constant 3")    mul = Operation("arith.mulf")    tanh = Operation("math.tanh")    log = Operation("math.log")     mul.inputs = [c1, c2]    tanh.inputs = [mul]    log.inputs = [c3]     c1.add_use(mul)    c2.add_use(mul)    c3.add_use(log)    mul.add_use(tanh)     ops = [c1, c2, c3, mul, tanh, log]    return ops, tanh  def main():    ops, root = build_sample_ir()    print("DCE 前 IR 操作数:", len(ops))    for op in ops:        print("  操作:", op.name, "| 使用者数:", len(op.users))     kept, removed = erase_dead_code(ops, [root])    print("DCE 后保留操作数:", len(kept))    print("DCE 删除死代码数量:", len(removed))    for op in removed:        print("  - 删除:", op.name)    print("最终输出 root 保留:", root in kept)  if __name__ == "__main__":    main()
目的:用一个极简 IR 模拟器复现 DCE 的“根收集 → 反向标记 → 删除”三步流程,运行后可直接看到哪些操作被标记保留、哪些作为死代码被删除。
  1. class Operation:定义极简 IR 节点,用 inputs 记录生产者、users 记录消费者
  2. def erase_dead_code(ops, roots):DCE 核心函数,从根集合开始反向标记
  3. marked = set()建立空集合存放被标记保留的操作
  4. stack = list(roots)用根操作(如最终输出 tanh)初始化工作栈
  5. stack.extend(op.inputs)把当前操作的生产者压入工作栈,实现反向传播
  6. if op in marked or op.has_side_effect:已标记或有副作用的操作必须保留
  7. removed.append(op)其余操作计入死代码列表
  8. build_sample_ir()构造包含死代码分支 log 的计算图
  9. print("DCE 后保留操作数:", len(kept))输出清点结果

常见误区

误区“这个打印操作的返回值没人用,删掉应该也没影响吧?反正没人读它的结果。”

正解恰恰相反,有副作用的操作正是 DCE 最不敢碰的对象。打印、写内存、修改全局状态,这些动作本身就在对外部世界产生“可见影响”。就像那瓶辣椒水,虽然没人“消费”它,但它随时可能喷出来惊动路人。删除它,等于把程序对外部环境说过的话抹掉了,程序的语义就变了。所以 DCE 的规则是:有副作用 = 绝对保留。

为何常见:初学 DCE 时容易只盯着“结果是否被使用”这半个条件,忽略了“副作用”是独立且更高的安全门槛,忘了根集合里早就包含所有有副作用的操作。

误区“DCE 都跑完了,怎么还有死代码?是不是实现有 bug?”

正解DCE 不是一次性手术,而是需要和其他 Pass 反复配合的长期保洁。因为常量折叠、CSE、内联等 Pass 在运行时会不断“制造”新的未使用操作。这就好比搬家中途会不断翻出“刚才没决定好”的东西,需要隔段时间再清一次。实践中 DCE 常被放入迭代循环,多轮执行后 IR 才会收敛到稳定状态。

为何常见:把 DCE 当成“跑一遍就完事”的独立工具,而没有站在整个优化流水线的视角理解死代码的“产生—暴露—清除”循环。

误区“死代码消除?不就是把程序里没人调用的函数删掉吗?”

正解DCE 的粒度要细得多——它作用在 MLIR 的单个操作(Operation)上,而不是整个函数。一段函数里可能 80% 的操作都在产出“无人消费”的中间结果,这些操作会被逐一删除,而函数本身则保留。它和垃圾回收也不同:GC 管的是内存对象的生命周期,DCE 管的是计算冗余的消除。就像整理行李时不是扔掉整个“箱中箱”,而是打开每一个小包,把里面装而不用的零碎物品挑出来。

为何常见:对 MLIR 的层级模型(模块→函数→区域→操作)不熟悉,容易把 DCE 与高级语言的“未引用函数删除”混为一谈。

误区“从头到尾扫一遍 IR,谁的结果没被用就删谁,不挺简单的吗?”

正解正向扫描存在一个致命问题:一个操作的结果虽然暂时被别的操作引用了,但如果那个“消费者”本身最终也被证明是死代码呢?正向扫描无法处理这种链条式的连锁死亡。DCE 采用反向标记正是为了从“确实活着”的根出发,只保留有真实存活依据的操作,其余一切未标记项全部清除。这就像打包时不是问“这包还有没有人要”,而是从“必须带的东西”出发反推支撑物,反推不到的统统扔掉。

为何常见:直观上觉得“扫一遍、看引用计数”就够了,没有意识到“活跃”必须从根的视角递归定义,单向扫描会漏掉深层冗余。

面试题

Q1. DCE 的主要优化目标是什么?

单选题 · easy

  • A.移除冗余且无副作用的操作← 正确答案
  • B.增加 IR 中操作的数量
  • C.减少模型训练的数据量
  • D.提升浮点运算的精度

答案:A。DCE 的目标是移除不影响最终输出的冗余计算——条件是无副作用且结果无人使用。B 与优化方向背道而驰,C、D 分别是数据与精度层面的问题,不属于 DCE 的工作范畴。就像打包顾问只负责扔没人用的东西,不会管你带多少食物。

Q2. 以下哪个操作一定不能删除?

单选题 · easy

  • A.无副作用且结果无人使用的乘法
  • B.有副作用且结果无人使用的打印← 正确答案
  • C.结果被活跃操作使用的加法
  • D.无副作用且结果被引用的常量

答案:B。有副作用的打印操作会对外部世界产生可见影响(如输出日志),即使结果无人使用也不能删。A 是典型死代码可删,C、D 都被活跃操作引用,属于“有人用”必须保留。类比:辣椒水喷雾虽然没人“消费”它,但它是危险品不能乱扔。

Q3. DCE 的反向标记阶段从什么集合开始?

单选题 · mid

  • A.从所有操作一起开始标记
  • B.从根操作集合反向开始标记← 正确答案
  • C.从输入常量直接开始标记
  • D.从随机操作触发开始标记

答案:B。DCE 的标记起点是“根集合”= 有副作用的操作 + 输出被外部消费的操作。从根出发反推生产者,才能保证标记链路的“存活”认证是有依据的。A、C、D 都缺少根集合这样的安全性锚点。

Q4. 反向标记时,若操作 op 被标记,下一步应该做什么?

单选题 · mid

  • A.标记其所有下游使用者
  • B.标记其所有上游生产者← 正确答案
  • C.标记其所在函数兄弟节点
  • D.标记其相邻内存位置

答案:B。被标记说明 op 是“活”的,那么为 op 提供输入的生产者也必须活下来——因此要标记所有上游生产者。下游使用者方向相反,是“谁消费我”而非“我依赖谁”。这就像带帐篷就要带支架,顺着依赖链往前推。

Q5. 某操作输出无人使用但带有副作用,DCE 应如何处理?

单选题 · mid

  • A.立即删除整个操作链
  • B.保留操作并标记其生产者← 正确答案
  • C.仅删除操作的输出结果
  • D.报错并终止编译流程

答案:B。有副作用的操作必须保留;同时它依赖的生产者也要被标记,否则删除生产者会让这个保留操作无法运行。类比:辣椒水必须带,而支撑它的包装(生产者)也不能拆。

Q6. 下列哪一项不是 DCE 带来的收益?

单选题 · hard

  • A.减小中间表示的体积
  • B.降低后端代码生成负担
  • C.直接提升模型推理精度← 正确答案
  • D.提升编译器流水线扩展性

答案:C。DCE 是保语义优化,不会改变程序行为,因此不会提升推理精度。A、B 是 DCE 的直接效果,D 是间接价值——DCE 兜底后其他 Pass 可放心产生临时结果。就像打包变轻了走得快,但不会让你变得更聪明。

Q7. 关于 DCE 与 CSE 的配合,以下哪种说法正确?

单选题 · hard

  • A.先CSE合并冗余再DCE清扫更高效← 正确答案
  • B.DCE会把CSE的结果全部还原
  • C.CSE与DCE功能上完全等价
  • D.CSE必须放在DCE之后执行

答案:A。CSE 合并重复计算后会留下“多余链路”,DCE 恰好清扫这些残骸,因此先 CSE 后 DCE 是经典搭配。B、C 混淆了两者职责,D 把顺序说反了。

Q8. 判断一个操作能否被安全删除,必要要求有哪些?

多选题 · mid

  • A.操作本身没有副作用← 正确答案
  • B.操作结果无活跃使用者← 正确答案
  • C.操作结果未写入外部内存← 正确答案
  • D.操作名称以 arith 前缀开头

答案:ABC。无副作用(A)、结果无活跃使用者(B)、结果未写入外部内存(C 本质是副作用的一种)都是安全删除的必要条件。D 与操作名称无关,即使 arith.constant 也可以保留,命名前缀不代表安全性。

Q9. 关于 MLIR 中的 DCE 实现,正确的说法有哪些?

多选题 · hard

  • A.DCE需要注册成Pass才能进入流水线← 正确答案
  • B.use_empty()可判断结果是否存在消费者← 正确答案
  • C.walk遍历覆盖Region内所有操作← 正确答案
  • D.DCE只会执行一次且不能重复运行

答案:ABC。Pass 注册是进入流水线的前提,use_empty() 是判断结果是否有消费者的核心 API,walk 负责遍历区域内所有操作。D 是错误认知——DCE 在复杂流水线中常被执行多轮。

Q10. 关于 DCE 与垃圾回收(GC)的区别,正确的说法有哪些?

多选题 · easy

  • A.DCE目标是清理计算冗余← 正确答案
  • B.垃圾回收管内存对象的生命周期← 正确答案
  • C.DCE不能删除有副作用的打印操作← 正确答案
  • D.DCE可以删除有副作用的调用

答案:ABC。DCE 清理的是“不会影响结果的冗余计算”,GC 管理的是“不再被引用的内存对象”,二者目标不同。有副作用的打印操作 DCE 不敢碰,所以 C 对、D 错。就像收拾行李和打扫房间是两码事。