想象你经营着一家连锁面包店。每天凌晨,中央厨房会揉出几十批面团,每一批都贴上独一无二的批次号,再通过冷链车送往各分店。每家分店的烘焙间墙上都挂着一块白板,上面写着:“当前案板上的这批面团,可能来自哪个批次?”如果一位面包师傅重新和了一团面,他就要擦掉旧记录、写上新的批次号;如果两家分店互相调货,收货方要把所有可能送来的批次都记在白板上——记录只多不少。这件看似琐碎的管理工作,正是编译器在优化程序前做的功课:程序里的每个赋值语句就像一次“和面”,变量的当前值像面团,而“现在这块面团可能来自哪一批”就是到达定义问题;“某款面包已经烤好、没变质、可以直接上架”就是可用表达式问题——能直接复用,就不用再烤一遍。数据流分析,就是给程序的每条“生产路径”建立一本动态台账的技术。
数据流分析是编译器优化与静态分析的基石。掌握到达定义、可用表达式与迭代求解框架,你就能看懂 LLVM 等现代编译器的优化管线底层逻辑,为成为编译器后端、静态分析平台或代码安全方向的 AI 从业者打下最扎实的地基。
可视化演示
数据流分析是编译器优化的基础,到达定义分析变量定义的传播路径,可用表达式识别冗余计算。本文详解数据流方程的建立... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Data F:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
数据流分析概述
要给十几家分店制定配送方案,你首先要回答:“每家分店的白板上,该记录哪些批次信息?”记录太少,前台可能向顾客推销根本不存在的面包;记录太乱,后厨可能把过期批次当新鲜批次。数据流分析要解决的,正是“在程序的哪个程序点上、应该维护什么样的事实、如何让事实在语句之间流动”。
程序可以被切成一个个基本块,当作一家家分店;基本块之间的跳转关系构成控制流图(CFG),当作配送路线图。每个基本块 B 有一个“入口信息” in[B](进门时白板上已有的记录)和一个“出口信息” out[B](出门时白板上的记录)。分析的目标,就是给每个程序点一个可证明成立的近似事实。
所有经典数据流问题都能纳入同一个“生成-消去”(gen-kill)模板:一块白板的更新动作只有三步——先记上新生产的批次(gen),再划掉因重新和面而作废的旧批次(kill),最后把仍然有效的记录合并起来对外传递。
所以,数据流分析并不关心面包最终卖给了谁,只关心“每一站之后,白板上可靠地写着什么”。有了这本台账,后面的一切优化——哪批面包可以提前烤、哪批面包根本不用烤——才有了决策依据。
- 入信息 in[B]进入基本块 B 时,从其所有前驱收集到的信息总和,类比进门时白板上的旧记录。
- 生成集 gen[B]在基本块 B 内新产生的信息,类比本班次新和的面团批次。
- 消去集 kill[B]在基本块 B 内作废的信息,类比被新面团覆盖的旧批次号。
- 出信息 out[B]离开基本块 B 时继续向后传递的信息总和。
- 并集与差集运算先用并集把新老有效信息合并,再用差集把失效信息剔除。
控制流图基础
连锁面包店的配送网络是一张有向地图:每家分店是一个节点,冷链车沿有向边送货;一家分店可能有多个上游供应商,也可能把货发给多个下游门店。程序的控制流图(CFG)正是这样一张“物流地图”,节点是基本块,有向边是执行流的跳转。
构造 CFG 的第一步是划分基本块。划分规则是:遇到跳转目标、跳转语句之后、return 语句之前就切开。每个基本块内部是顺序执行的语句,没有分叉;执行流只能在基本块的边界处进入(入口)或离开(出口)。程序点(program point)则被定义为“相邻语句之间”的位置,每个程序点的信息就是该点白板上的记录。
当多个前驱把一个基本块作为共同的下游时,收货方必须把上游所有可能送来的信息都纳入账本。这就是“汇合运算”:入口信息由所有前驱出口信息汇总而来。
路线图定了,各店才能对账。CFG 是数据流分析的“物流地图”:没有它,白板上的信息不知道该往哪送、该和谁合并。而有了这张图,后面的每一条数据流方程都只是“在地图上照着边传播信息”而已。
- 前驱集合 pred(B)所有能直接流向 B 的基本块,类比向这家分店供货的上游门店。
- 出口信息 out[P]前驱 P 离开时白板上的信息。
- 并集汇合到达定义类分析用并集,表示“任何一条路径带过来的信息都算数”。
到达定义分析
现在回到开头的场景:面包师傅拿起一块面团,最关心的问题是——“这块面团到底是哪一批?是凌晨中央厨房送来的 d1 批,还是隔壁店调来的 d2 批?”在程序里,这对应到达定义(reaching definitions)问题:某个赋值语句(定义)d: x = …… 产生的值,能否沿着一条执行路径到达当前的程序点,而不被中间对 x 的再次赋值覆盖?
一个基本块内如果有定义 d(例如 x = a + b),那么它会生成信息 d;同时,变量 x 的其他所有定义都被它杀死——因为 x 的值已经被覆盖了。放在面包店语境里:一位师傅重新和了一团面,旧批次的记录就必须从白板上擦掉。于是到达定义的传递函数就有了明确语义:
“追批次号”带来的直接收益是:如果某个定义永远到达不了任何使用点,那它就是死代码,可以安全删除;如果某个变量的到达定义只有一个常量赋值,还能做常量传播。面包店同样如此——查出“这批面团从没被任何分店用过”,就可以直接报废处理,省下冷藏空间。
- 入口集合 RD_in[B]进入 B 时仍然“存活”的定义集合,即所有可能还没被覆盖的面团批次。
- 生成集 gen_BB 内新出现的定义 d,是 B 对变量的新赋值。
- 消去集 kill_BB 内对变量 x 赋值时,所有其他对 x 的定义都被杀死。
- 出口集合 RD_out[B]新定义加上没被杀死的旧定义,组成离开 B 时的集合。
可用表达式分析
面包店常遇到一个幸福的烦恼:某款招牌面包,A 分店已经烤好一批,B 分店也刚好烤好一批——那新来的订单还用重新烤吗?前提是:所有可能的供货路径上,这款面包都烤好了,而且配方没被改动。只要有一条路径没烤,你就不能承诺“直接上架”。程序里的可用表达式(available expressions)分析,就是在判断“表达式 e 的计算结果能否直接复用”。
可用表达式的定义是:从入口到当前点的所有路径上,都计算过表达式 e(如 a + b),并且之后 a、b 没有被重新赋值。基本块内新计算出的表达式放入 gen;而一旦某个操作数变量被重新赋值,所有包含它的表达式都会被 kill——注意,kill 的对象是操作数变量,不是表达式本身。
可用表达式是“必须”分析,它的汇合运算因此而特殊:
可用表达式分析是全局公共子表达式消除的基础:当编译器发现某个表达式在当前位置“家家有货且没变质”,就会把重复计算替换成对之前结果的引用,省下一整条加法指令。面包店也因此敢在高峰期提前批量烤制招牌面包,而不是每位顾客下单都从头和面。
- 入口集合 AE_in[B]进入 B 时,在全部到达路径上都有效可用的表达式集合。
- 生成集 gen_BB 内新计算的表达式,且其操作数在计算之后没有被重定义。
- 消去集 kill_B操作数变量在 B 内被重新赋值后,所有包含它的表达式失效。
- 出口集合 AE_out[B]本块新生成的表达式,加上进来时仍有效的表达式。
前向分析框架
面包店的账本有两种记法:一种是顺着营业时间,从开门记到打烊,这叫前向;另一种是从打烊倒推,盘算“哪些准备工作是收尾时必然要用的”,这叫后向。数据流分析同样有两种方向。到目前为止的到达定义、可用表达式都是前向分析:信息从基本块的入口流向出口,再流向所有后继。
把前向问题的共性抽出来,就得到了一个统一框架。每个数据流问题由三要素决定:方向(前向还是后向)、汇合运算(并集还是交集)、传递函数(gen-kill 模板)。汇合运算的选择取决于语义:到达定义是“可能”(may)分析,用并集;可用表达式是“必须”(must)分析,用交集。
我们用 ⊔ 表示抽象的汇合运算,那么前向分析的方程结构可以统一写成:
这个框架的美妙之处在于:你只要告诉框架“方向 + 汇合 + 传递函数”,一套求解引擎就能同时处理到达定义、可用表达式、非常忙表达式等所有问题。配送流程亦如此:“收货时算一次总账(汇合)、店内按规则更新(传递函数)”,无论哪家分店,管理动作都是同一套标准流程。
- 汇合运算 ⊔把多个前驱的信息合并成一个入口信息,可以是并集也可以是交集。
- 传递函数 f_B基本块 B 的信息更新规则,等同 gen-kill 模板。
- 前向语义先算入口,再算出口,信息顺着执行流往后传。
迭代求解算法
连锁店刚开张时,每家分店的白板都是空白的。于是店长定下规矩:每天傍晚,各店把白板拍照发到工作群,各家照着别家送来的信息更新自家白板;一旦有人更新了,又要通知其他店再更新一轮。如此反复,直到某天傍晚,没有任何一家店再改动白板——这个状态就叫收敛。
计算机也是这样求解数据流方程的:给每个基本块的出口信息设一个初始值(到达定义类分析通常从空集出发,保证安全),然后反复套用数据流方程,直到所有集合不再变化。每一轮迭代,信息就沿着控制流边多扩散一层;由于程序里的定义个数有限,扩散过程必然在有限轮内结束。
把 n 个基本块的全部数据流方程看成一个整体函数 F,迭代过程就是不断把上一轮的完整结果代回方程:
实际工程中还有优化手段——工作表算法(worklist):只把“出口信息发生变化”的基本块放入待处理队列,只重新计算这些块的受影响后继,避免每轮都扫描全部基本块。面包店的工作群也一样:只有自家白板真改动了的分店才需要发新照片,没改的就不用打扰大家。
- 整体状态 x把所有基本块的出口信息拼成一个向量,好比各分店白板的合订本。
- 整体函数 F用所有数据流方程把当前快照更新为下一轮快照。
- 收敛判定连续两轮的快照完全相同,说明信息已扩散到极限。
格与单调性
白板上的信息是有“精确度层级”的:完全空白(什么都不知道)是最底层;知道“可能来自 d1 或 d2”是中间层;知道“一定来自 d1”是更精确的层级。数据流分析把这套层级结构抽象成格(lattice):底层记作 ⊥(bottom),顶层记作 ⊤(top),偏序关系 ⊑ 表示“信息更精确”。
数据流的奇妙性质是单调性:如果一份输入信息比另一份更精确,那么经过同一个基本块的传递函数后,输出信息不会变得更粗糙。换句话说,好账本只会越记越全,绝不会把已经确认的记录抹掉。这正是迭代能收敛的理论基石。
格理论回答了那个关键问题:“白板传消息的活动,凭什么必然在某天停下来?”答案是:信息只在有限高的格上单调上升,而单调上升不可能无限持续。于是“迭代求解”从一个工程技巧,升格成了有严格数学保证的算法。
- 偏序关系 ⊑表示“信息不精确于”,从 ⊥ 到 ⊤ 构成精确度阶梯。
- 单调函数信息越精确的输入,绝不会产生更粗糙的输出。
- 最小不动点 lfp(f)从 ⊥ 出发反复迭代得到的稳定解。
非常忙分析
打烊前的面包店要做收尾:可能走“剩余面团回冷库”流程,也可能走“制作明日试吃品”流程。但无论走哪条流程,“预热烤箱”这个动作都是必经的。聪明的店长会提前把烤箱预热提到所有分叉之前——反正迟早要做,不如早做。程序里叫代码提升(hoisting),而识别这种“所有后续路径都必然计算”的表达式的技术,就是非常忙表达式(very busy expressions)分析。
非常忙表达式的定义是:从当前点出发的所有路径上,在操作数被重新定义之前,都一定会计算该表达式。它需要从后往前分析:先看基本块的所有后继,再回推本块的入口。汇合运算发生在出口处,取交集——因为必须所有后继路径都“需要”才算数。
非常忙分析与活跃变量分析同属后向分析家族:活跃变量是后向 may 分析(用并集),判断“哪些变量之后还会被用到”,用于寄存器分配;非常忙表达式是后向 must 分析(用交集),用于代码提升。至此,面包店的故事完整闭环了:顺时间记账、倒时间盘账、只增不减的台账、直到全网稳定——数据流分析的全部核心思想,就是这套“给程序点建账本”的方法论。
- 后继集合 succ(B)B 之后所有可能执行的基本块。
- 出口信息 VB_out[B]离开 B 后,所有路径上都一定会计算的表达式集合。
- 入口信息 VB_in[B]由出口信息反推回入口,仍是 gen-kill 模板。
AI 应用场景
编译器后端优化管线(LLVM)
阶段:编译优化与代码生成
解决的问题:手写优化 pass 时,开发者难以直观掌握每个程序点上的定义-使用关系;到达定义分析能自动构建“变量从哪个赋值来”的完整图谱,支撑常量传播、死代码消除等优化,避免人工逐块审查的低效与遗漏。
具体案例:LLVM 的优化管线中,-O2 级别会运行多个依赖数据流分析的 pass,如死代码消除(DCE)利用到达定义判断“某定义不再到达任何使用点”,从而安全删除冗余赋值;全局值编号(GVN)则基于可用表达式思想,把相同的重复计算折叠为一次。AI 生成的代码往往包含大量重复赋值,这类优化能显著减小编译产物尺寸。
代码安全审计中的污点分析
阶段:静态分析与风险检测
解决的问题:人工审计难以在海量代码里追踪“用户输入从哪里来、流经哪些语句、最终到达哪个危险函数”;数据流传播(到达定义的思路)可以自动把“脏数据”的定义传播到所有使用点,标记出未净化的注入路径。
具体案例:安全团队审查一个 AI 自动生成的 Web 服务代码时,用污点分析工具追踪 request.getParameter() 这个定义的传播路径,发现它绕过了校验函数直接进入 SQL 拼接语句,从而定位 SQL 注入漏洞。这与到达定义分析在机制上完全同构——只不过“定义”换成了“污染源”。
智能计算引擎的公共子表达式消除
阶段:查询优化与计算加速
解决的问题:AI 数据管线中,同一个特征计算(如 log(x + 1))可能在多个分支重复执行,浪费昂贵算力;可用表达式分析能识别“所有路径都已算过且值未变”的计算,将其结果缓存复用。
具体案例:Spark SQL 的优化器在生成物理计划前,会扫描关系表达式树,把重复的投影、过滤表达式合并;其理论基础正是可用表达式分析——只有保证所有数据分片都已完成相同计算,才能安全复用中间结果。对每日运行数小时的 AI 训练数据预处理任务,这种优化可节省 10%-30% 的计算时间。
Python 演示
def reaching_definitions(gens, kills, preds, n): """迭代求解到达定义分析,返回 (迭代轮数, 各基本块出口集合)。 gens[b] / kills[b] 是集合,preds[b] 是前驱基本块编号列表, n 是基本块数量。从空集出发迭代到不动点。 """ out = [set() for _ in range(n)] changed = True iterations = 0 while changed: changed = False iterations += 1 for b in range(n): ins = set() for p in preds[b]: ins |= out[p] new_out = gens[b] | (ins - kills[b]) if new_out != out[b]: out[b] = new_out changed = True return iterations, out def main(): n = 4 # 菱形 CFG: B1 分叉到 B2/B3,再汇合到 B4 # 定义点: d1: x = a (B1), d2: x = a - 1 (B3), # d3: y = x + 1 (B1), d4: x = x * 2 (B2) gens = [{"d1", "d3"}, {"d4"}, {"d2"}, set()] kills = [{"d2", "d4"}, {"d1", "d2"}, {"d1", "d4"}, set()] preds = [[], [0], [0], [1, 2]] iterations, out = reaching_definitions(gens, kills, preds, n) for b in range(n): print("B{} 出口到达定义: {}".format(b + 1, ", ".join(sorted(out[b])))) print("共迭代 {} 轮后收敛".format(iterations)) if __name__ == "__main__": main()
out = [set() for _ in range(n)]出口信息初始化为空集,这是 may 分析的安全起点(最小不动点)。while changed:只要有任何一个基本块的信息发生变化,就继续下一轮,这正是不动点迭代的主循环。ins |= out[p]汇合运算:把每个前驱出口的定义并入入口集合,对应公式中的并集。new_out = gens[b] | (ins - kills[b])传递函数:本块新定义,加上未被杀死的旧定义。if new_out != out[b]:检测本轮是否有变化,变化是继续迭代的唯一动力。preds = [[], [0], [0], [1, 2]]用前驱表描述菱形控制流图,B4 同时有 B2、B3 两个前驱。
常见误区
误区初学者常以为到达定义分析能告诉程序“此刻变量 x 实际上等于几”,就像看监控录像一样精确。于是看到“多个定义到达同一个点”时,会困惑“程序到底该用哪一个?”
正解到达定义回答的是“x 的当前值可能来自哪个赋值语句”,注意“可能”二字。它是静态近似:把所有能到达当前点的路径都算进去,甚至包含运行时根本不会走的路径,结果是一个集合而不是单一取值。就像白板上记的是“所有可能的供货来源批次”,而不是“最终上架的那一袋”。要判断“实际取哪个值”,需要再做常量传播等后续分析。
为何常见:人们习惯用运行时的确定性思维理解编译期抽象,忽略了静态分析的本质是“安全近似”——宁可多报,不可漏报。
误区“到达定义入口处用并集,那可用表达式入口处也把所有前驱的表达式并起来就好,反正多记点没坏处。”
正解可用表达式是 must 分析,入口汇合必须用交集。只有所有前驱都计算过 a + b 且操作数未变,当前点才能放心复用;只要有一条路径没算过,就必须当作不可用。若误用并集,编译器可能在某个没算过的路径上引用不存在的计算结果,产生致命错误。好比分店不能因为“某一家分店有货”就向所有顾客承诺随时能买到。
为何常见:学完到达定义后容易把并集当作默认汇合操作,忘了 may/must 的语义差异决定了运算是并还是交。
误区“前向才是正常的分析方式,后向大概是进阶技巧,不如前向重要。”
正解分析方向只取决于问题本身,与难度无关。信息从执行顺序之前传向之后用前向(到达定义、可用表达式);信息从之后传回之前用后向(非常忙表达式、活跃变量)。后向分析同样基础、同样常用——LLVM 的活跃变量分析用于寄存器分配,非常忙分析用于代码提升,都是编译器中不可或缺的后向数据流分析。
为何常见:教材通常先讲前向,学生容易把教学顺序误当成优先级或难度顺序,从而低估后向分析的地位。
误区“可用表达式分析里,如果块里有 a = b * c 这条赋值,那 a 这个表达式就被 kill 了,后面不能再复用。”
正解可用表达式的 kill 发生在操作数变量被重新赋值时,而不是表达式本身。赋值 a = b c 会杀死所有包含 a 的表达式(如 a + b、a d),同时会生成表达式 b * c(前提是其后 b、c 未被重定义)。也就是说,kill 的对象是“依赖某个变量的所有旧表达式”,以变量为单位做失效,而不是以表达式本身为单位。
为何常见:初学者把“语句”和“被赋值的变量”混为一谈,忽略了 kill 的语义是“操作数失效导致旧计算不可复用”。
面试题
Q1. 数据流分析中,基本块传递函数的统一 gen-kill 模板是?
- A.
out[B] = gen[B] ∪ (in[B] − kill[B])← 正确答案 - B.
out[B] = gen[B] ∩ (in[B] − kill[B]) - C.
out[B] = gen[B] ∪ (in[B] ∩ kill[B]) - D.
out[B] = kill[B] ∪ (in[B] − gen[B])
答案:A。gen-kill 模板的核心语义是“新产生的信息 ∪(进来的信息 − 被作废的信息)”。A 完全匹配;B 错误地把并集换成交集;C 对 kill 使用了错误的运算;D 颠倒了 gen 与 kill 的位置。类比白板更新:先记新批次(gen),再划掉旧批次(kill),剩下的加上新的一起传出去。
Q2. 当赋值语句 d: x = y + 1 出现后,它对变量 x 的其他定义产生什么影响?
- A.生成其他定义
- B.杀死其他定义← 正确答案
- C.复制其他定义
- D.不影响其他定义
答案:B。x 被重新赋值意味着 x 的旧值被覆盖,因此所有其他对 x 的定义都被“杀死”,无法再到达后续程序点。这就像师傅重新和面后,旧批次的记录必须从白板上擦掉。A、C、D 都与“新定义覆盖旧值”的语义矛盾。
Q3. 可用表达式分析在基本块入口处的汇合运算应该是?
- A.并集(∪)
- B.交集(∩)← 正确答案
- C.对称差集
- D.笛卡尔积
答案:B。可用表达式是 must 分析,要求“所有路径上都被计算过”才能判断可用,因此汇合用交集。若用并集,只要某一条路径算过就会被误判为可用,可能导致不安全优化。这正是“全路径保留”的含义——只有家家有货才敢承诺上架。A 混淆了 may 与 must;C、D 没有数据流语义。
Q4. 非常忙表达式分析属于哪一类数据流问题?
- A.前向、may 分析
- B.前向、must 分析
- C.后向、must 分析← 正确答案
- D.后向、may 分析
答案:C。非常忙表达式要判断“所有后续路径都必然计算该表达式”,信息从后继回推到前驱,所以是后向;“所有路径”意味着 must 语义,出口汇合用交集。它用于代码提升,和后向 may 的活跃变量形成对照。A 与 B 方向错误;D 的 may 语义错误。
Q5. 迭代数据流算法将所有出口信息初始化为空集时,最终收敛到的是?
- A.最小不动点← 正确答案
- B.最大不动点
- C.任意一个不动点
- D.必然不收敛
答案:A。从格的底部 ⊥(空集)出发,在单调函数作用下反复迭代,Kleene 定理保证收敛到最小不动点。对到达定义等 may 分析,最小不动点恰好是安全且最精确的结果。从全集出发则会收敛到接近最大不动点的结果,适合 must 分析的初始化。C、D 与迭代理论不符。
Q6. 以下哪一项不属于经典的数据流分析问题?
- A.到达定义
- B.可用表达式
- C.活跃变量
- D.指令调度← 正确答案
答案:D。到达定义、可用表达式、活跃变量、非常忙表达式都是经典数据流问题,共享 gen-kill 框架和迭代求解。指令调度是确定机器指令顺序的优化技术,属于指令级并行优化,不做程序点信息传播。D 与其他三项不在同一知识分类下。
Q7. 基本块 B2 的前驱是 B1 与 B3,则到达定义分析中 in[B2] 如何计算?
- A.
out[B1] ∪ out[B3]← 正确答案 - B.
out[B1] ∩ out[B3] - C.
out[B1] − out[B3] - D.只取
out[B1]
答案:A。到达定义是 may 分析,入口信息由所有前驱出口信息的并集汇合而成——任何一条供货路径带来的定义都要记录。B 是 must 分析的写法;C 是集合差,没有意义;D 漏掉了 B3,会导致信息不完整、分析不安全。
Q8. 关于可用表达式分析,下列说法正确的有?
- A.gen 要求表达式操作数在块内计算之前未被重定义← 正确答案
- B.操作数变量被重新赋值会使包含它的表达式被 kill← 正确答案
- C.汇合运算采用并集
- D.可用于全局公共子表达式消除← 正确答案
答案:ABD。可用表达式的 gen 需要“计算发生时操作数仍保持原值”,所以 A 正确;kill 以操作数变量为单位,B 正确;该分析是 must 分析,汇合必须用交集,C 错误;识别可复用计算正是全局公共子表达式消除的基础,D 正确。
Q9. 以下哪些是迭代数据流算法收敛的保证条件?
- A.信息构成的格高度有限← 正确答案
- B.传递函数具有单调性← 正确答案
- C.汇合运算是结合且交换的← 正确答案
- D.程序必须不含循环
答案:ABC。有限高的格保证信息不能无限上升;单调函数保证迭代不会“倒退”;结合且交换的汇合运算保证多路信息合并结果与合并顺序无关。三者共同保证从初始点出发必然到达不动点。D 错误——数据流分析恰恰要处理含循环的程序,循环边上的信息传播正是迭代存在的意义。
Q10. 关于后向数据流分析,说法正确的有?
- A.非常忙表达式分析是后向 must 分析← 正确答案
- B.后向分析中
in[B]由out[B]经传递函数得到← 正确答案 - C.后向分析一定使用并集汇合
- D.活跃变量分析是后向 may 分析← 正确答案
答案:ABD。非常忙表达式从后继回推前驱,汇合用交集,A 正确;后向分析把 gen-kill 模板倒过来,in[B] = gen ∪ (out[B] − kill),B 正确;后向分析的汇合运算同样由 may/must 决定,活跃变量(may)用并集、非常忙表达式(must)用交集,C 错误;活跃变量确实在后向传播“以后可能被使用”的信息,D 正确。