想象你是一家大饭店的主厨。你的后厨有几百道菜,每道菜都写在一张菜谱上:先洗菜、再切菜、开火、放调料……有一天,采购部通知你:酱油换牌子了。你脑子里立刻冒出一个问题——哪些菜的味道会变? 要回答这个问题,光靠一摞菜谱是不够的:菜谱只写了每道菜自己的步骤顺序,却写不出「这道菜出锅的酱汁,是下一道菜腌肉的原料」,也写不出「只有在客人点了辣味套餐时,这道菜才会被端上桌」。你需要一张工序关系图:把每个步骤画成节点,用箭头标出「谁把半成品递给谁」(原料流动),以及「谁开了开关、谁才被执行」(条件闸门)。这张图,就是程序世界里的程序依赖图(Program Dependence Graph,PDG)。软件工程师和 AI 代码助手也靠同样的思路理解程序:改一行代码会波及谁?Bug 的根源藏在哪?这篇文章,就带你从零看懂这张「后厨地图」。
掌握程序依赖图,等于拿到「让 AI 真正读懂代码因果关系」的钥匙——它是程序切片、缺陷定位、变更影响分析、代码安全扫描等代码智能技术的共同地基,也是你从「会写代码」进阶为「会分析代码」的关键一步。
目录
可视化演示
展示程序依赖图PDG的构建流程、程序切片分析与安全漏洞检测,交互切换查看各阶段核心指标。
本节配有交互动效(进入视口后自动加载,约 3KB)。
PDG构建流程:从源码到依赖图 AST语法树经控制流分析构建CFG控制流图 数据流分析融合控制依赖与数据依赖生成PDG PDG=CDG∪DDG 程序依赖图是程序切片和影响分析的核心数据结构
基本概念:一张「后厨工序图」
主厨画后厨地图时,不会把菜谱整段抄上去,而是把每一个操作步骤(洗菜、切姜、下锅、装盘)都画成一个小圆圈,再用两种箭头把它们连起来:红色箭头表示「只有前面那道判断通过了,这一步才会被执行」,绿色箭头表示「这一步的成品会作为原料流入那一步」。这张只关心因果、不关心「谁站在谁旁边」的图,就是后厨的工序关系图。
程序世界里,「步骤」就是源代码里的语句(statement)和判断条件(predicate)。把每个语句画成一个节点,把上述两种因果关系画成有向边,得到的图就叫程序依赖图(PDG)。它最早由 Jeanne Ferrante 等人在 1987 年提出,目的是把程序的语义依赖从代码的物理书写顺序中解放出来——就像工序图把因果从菜谱的排版里解放出来一样。
PDG 由两部分构成:数据依赖边记录「谁把值传给谁」,控制依赖边记录「谁决定谁是否执行」。二者合并,就得到一张同时携带数据流与控制流信息的程序表示。与控制流图(CFG)相比,PDG 不强调执行顺序的线性排列——顺序被「编码」进了依赖关系里:如果 A 必须比 B 先执行,那一定是因为 B 在数据上或控制上依赖于 A。
回到后厨:有了这张工序图,主厨不再需要按「菜谱页码」找答案,只需要沿着箭头看因果。接下来我们要做的,就是弄清这两种箭头分别是怎么产生的。
- N(节点集合)程序里的每个语句、每个判断条件都被抽象成一个节点。
- E_c(控制依赖边)从「判断条件」指向「由它决定是否执行的语句」的边。
- E_d(数据依赖边)从「写入变量 v 的语句」指向「读取 v 的语句」的边。
- ∪(并集)两类边合并成完整的依赖图,且互不重叠。
数据依赖:原料接力赛
后厨里最常见的因果是「供料」:炖牛腩的汤底,要用来烧豆腐;切好的姜丝,要撒进蒸鱼。如果姜丝切得不对,蒸鱼的味道就变了——这就是原料接力。后厨地图上的绿色箭头,画的就是这种半成品传递。
程序里的「原料」是变量(variable)。一个语句写入(定义)变量 v,另一个语句读取(使用)变量 v,且中间没有被再次写入,那么后者就在数据上依赖前者。这种关系叫数据依赖(data dependence),也叫定义-使用关系(def-use relation)。
数据依赖分为三种:流依赖(flow dependence,写后读 RAW)、反依赖(anti-dependence,读后写 WAR)和输出依赖(output dependence,写后写 WAW)。其中流依赖是「真依赖」——它代表值的真实传递,无法通过改名消除;反依赖和输出依赖是「假依赖」,源于变量被复用,可以通过引入新变量或重命名来消除,编译器做寄存器重命名时就会用到这个性质。检查「中间是否有重定义」是判定流依赖的关键:它要求从写入语句到读取语句的路径上,v 没有被任何语句覆盖,即找到的是「最近的写入者」。
想象一锅高汤:只要中间没人把汤倒掉重新换水,端到下一道菜面前的,就是最初那锅汤。数据依赖就是在回答:「这道菜用的,到底是哪一锅汤?」
- Def(S_i, v)(定义)语句 i 对变量 v 赋值,v 的值从这里「出生」。
- Use(S_j, v)(使用)语句 j 读取 v 的值参与计算。
- path(S_i, S_j)(路径)从 i 到 j 在 CFG 上要经过的所有语句。
- ¬Def(S_k, v)(中间无覆盖)路径上每个中间语句都没有重新定义 v,保证 j 读到的是 i 写的那个值。
控制依赖:厨师长的「开关」
后厨里还有一种因果与原料无关:「只有客人点了辣味套餐,这道水煮鱼才会下锅;没点,鱼就养在水池里。」水煮鱼并不「消费」点单这个动作的产物,但它的生死由点单决定——这就是开关关系。
程序里,「点单」对应判断条件(predicate),比如 if 语句。一个语句是否会被执行,取决于判断条件走哪个分支。这种「是否执行由谁决定」的关系,就是控制依赖(control dependence)。它建立在控制流图(CFG)的基础上,核心工具是后支配(post-domination)关系:从某个节点出发,无论走哪条路径,最终都必然经过另一个节点。
基于后支配,控制依赖的精确定义是:存在 i 的一个后继 p,使得 j 后支配 p,但 j 并不后支配 i 本身。这一定义刻画了「j 是否执行,取决于从 i 走了哪条路」。特别要注意循环结构:循环头(loop header)控制着回边上的语句是否继续执行,因此循环体末尾的语句也控制依赖于循环条件。
所以不是「写在 if 大括号里的就算被控制」——就像后厨里,无论客人点不点辣,米饭都要蒸,米饭就不受「点辣」开关控制。只有那些「选了这个分支才做、选了那个分支就跳过」的菜,才真的被开关捏在手里。
- succ(S_i)(分支出口)i 在 CFG 中的直接后继节点集合,即「分岔出去的路」。
- PDOM(S_j, p)(后支配支路)从支路 p 出发的每条路径最终都经过 j,走这条路就躲不开 j。
- ¬PDOM(S_j, S_i)(不后支配源头)存在从 i 出发却不经过 j 的路径,说明 j 不是「无论如何都会执行」。
- ∃p(存在判定)两条支路中至少有一条被 j 锁死,j 才真正被 i 的抉择「牵着走」。
构建算法:两步画出工序图
主厨要画后厨工序图,不会一次画出所有箭头。他先按动线把灶台、案板、蒸锅的位置摆好(相当于流程骨架),再一道菜一道菜地核对:这道菜用了哪锅汤?那笔订单决定了哪道菜下锅?分两轮检查,才画得干净。
PDG 的构建也分两大步,且顺序有讲究:第一轮在 CFG 上计算后支配关系,生成控制依赖边;第二轮做数据流分析,计算定义-使用链,生成数据依赖边。两轮结果合并,即是完整 PDG。
第一轮中,控制依赖的计算流程是:构造 CFG → 求后支配树 → 对每个节点考察其每个后继,用前述定义判定依赖。第二轮中,数据依赖的计算常用到达定义(reaching definitions)数据流方程:对每个基本块 B,求出「进入 B 时仍然活跃的定义集合 In[B]」与「离开 B 时的定义集合 Out[B]」。如果一个定义能到达某个使用点且未被杀死(kill),就生成一条数据依赖边。为了提高精度,还可以加入别名分析、过程间分析等手段,减少「误报」的依赖边。
就像主厨核对原料:进后厨时哪些高汤还活着,被哪道菜用掉、被哪道菜换掉,最后哪些汤还能传给下一道菜——方程里的每个符号,都是后厨账本上的一行。
- In[B](块入口定义集)所有前驱块流出的定义在 B 入口处汇合,取并集。
- Gen[B](本块生成)B 内部新产生的变量定义,且在本块内没有被再次覆盖。
- Kill[B](本块杀死)B 内对 v 的重新定义,会杀掉所有更早到达的 v 的定义。
- Out[B](块出口定义集)入口活跃的定义减去被杀死者,并上新生成者,就是本块交给后继的「接力棒」。
程序切片:只动「相关」的步骤
客人投诉水煮鱼太麻。主厨不会把整个后厨翻一遍,而是沿着工序图的箭头往回走:鱼是谁腌的?花椒是谁放的?腌制用的料酒是哪批?只有这些「影响麻味」的步骤才需要检查。这张「与某个结果有关的步骤子集」,就是后厨世界的切片。
程序里,这个操作叫程序切片(program slicing)。给定切片准则(slicing criterion)——一个语句 S 和一组变量 V——程序切片就是求「所有可能影响 S 处 V 取值的语句集合」。这个集合叫后向切片(backward slice),它在调试与缺陷定位中大显身手。
程序切片的价值在于:它把一个庞大的程序约简为与当前关注点相关的子集,且保证在切片准则上的行为与原程序一致。在 PDG 上,切片计算变成了一次可达性(reachability)问题:从准则节点出发,沿数据依赖边和控制依赖边反向遍历,所有能到达的节点构成后向切片;反过来,从准则节点沿依赖边正向遍历,所有被它影响的节点构成前向切片(forward slice)。经典的 Weiser 切片与基于依赖图的 Ottenstein–Ottenstein 切片是两种主要流派,后者更高效,因为它把问题归结为图可达性,一次遍历即可完成。
后向切片是「查上游」:这盘鱼太麻,往上追到花椒;前向切片是「看下游」:如果换掉这批花椒,哪些菜会跟着变味。方向不同,用途不同,但都在这张因果网上走路。
- S(切片准则的语句)我们关心行为的目标语句,例如出错的 print 或可疑的赋值。
- V(关注的变量集)我们关心这些变量在 S 处的取值。
- E(依赖边集合)切片沿数据流与控制流的因果链传播,两类箭头都算数。
- n →* S(可达闭包)从 n 出发沿 E 走零步或多步能到 S,n 就入选切片。
影响分析:改一处,波及全厨房
采购部通知酱油换牌子。主厨最关心的是:这瓶新酱油进了哪些菜?哪些菜的味道会因此改变?他会从「酱油」这个节点出发,沿着工序图的箭头往前推,被波及的菜,都要重新试菜、重新上菜单。
程序里,「换酱油」就是一次代码变更(change),「重新试菜」就是回归测试(regression testing),而「找出哪些代码会受影响」的过程叫变更影响分析(change impact analysis)。它的核心技术正是前向切片。
设变更涉及的语句集合为 Δ,影响集就是 Δ 中每个语句的前向切片的并集。被影响到的语句,其执行结果可能改变,因而需要重新测试。缺陷定位(fault localization)则是影响分析的「镜像」用法:从失败点出发做后向切片,把嫌疑代码从整个程序缩小到切片范围内。在代码安全分析中,污点分析也依赖 PDG:把用户输入标记为污点(taint),沿数据依赖边追踪它是否流入危险函数(sink),从而发现注入漏洞。
就像试菜清单——酱油涉及的菜划勾,没涉及的菜照常出餐。影响分析让 AI 工程师在动代码之前,先看清「这一改,会惊动后厨里的谁」。
- Δ(变更集)一次提交或重构中所有被修改的语句。
- All(s)(全变量集)s 处所有变量都值得关注,因为变更可能影响任意输出。
- ForwardSlice(前向切片)从 s 出发沿依赖边正向可到达的语句,即「被 s 影响」的集合。
- ⋃(并集)多处变更的影响区重叠也没关系,并集给出总爆炸半径。
工程挑战:千人厨房的效率难题
上面讲的是几十道菜的小厨房。现实中,大厂的后厨有上百万道菜:几千位厨师同时改菜谱、几万道菜共享原料池。要全量画一张覆盖所有因果的工序图,光箭头就可能画出几十亿条——纸都不够用。主厨必须学会「偷懒」:只重画被改动的区域,用近似图代替精确图。
程序仓库也一样:Linux 内核这类千万行级项目,所有语句构成的全量精确 PDG,其依赖边在最坏情况下是 O(n²) 量级,构建与存储都难以承受。工程化 PDG 的三大主题是:大规模构建、增量更新、精度-效率权衡。
针对大规模构建,主流思路是模块化分层:先为每个函数构建过程内 PDG,再用调用图(call graph)建立过程间依赖,按需解析别名,避免全量精化。针对增量更新,增量 PDG 构建只对变更语句所在函数重建依赖边,再沿调用边传播变更标记,使单次更新成本从 O(n²) 降到接近 O(Δ)。精度-效率权衡方面,工业级分析器常采用流不敏感或上下文不敏感的近似,牺牲少量精度,换取毫秒级分析与可控内存。
千人厨房里的工序图永远画不完,但聪明的后厨只维护「最近改动区域的因果网」,并用足够的近似保证大多数时候判断正确。理论告诉你能画多准,工程告诉你在有限时间里画得多快——两者之间的缝,正是 AI 代码助手的用武之地。
- n(语句规模)程序规模的基本度量,百万行代码即 n 达百万级。
- O(n²)(最坏边数)任意两语句之间理论上都可能产生依赖边,边数是节点数的平方级。
- Δ(变更规模)一次提交实际改动的语句数,通常远小于 n。
- O(|Δ|)(增量目标)理想情况下,更新成本只与改动规模挂钩,与总仓库大小解耦。
AI 应用场景
场景1:AI 代码助手的缺陷定位
阶段:代码理解与诊断
解决的问题:面对一个失败的测试用例,AI 助手要回答「bug 在哪里」。传统做法是让工程师在数千行日志里人肉搜索,或把整个文件喂给大模型「猜」。基于 PDG 的后向切片,AI 先把失败断言(如 assert x == 5)作为切片准则,得到影响 x 取值的语句集合,再把模型注意力集中到切片内的候选行,定位准确率和推理成本都大幅改善。
具体案例:Meta 的内部调试工具使用动态后向切片分析崩溃堆栈对应的语句,辅助工程师在几分钟内定位生产环境崩溃根因;国内某编译器团队把 PDG 切片嵌入 AI 插件,在「空指针异常」场景下将嫌疑代码从平均 200 行缩小到 15 行左右。
场景2:开源代码的安全漏洞扫描
阶段:静态分析与安全审计
解决的问题:开源组件「投毒」事件频发,人工审计不可能读完全部依赖。基于 PDG 的污点分析把「用户可控输入」标记为污点源,沿数据依赖边追踪它是否到达危险函数(如 eval、SQL 拼接),同时用控制依赖边过滤掉「即使数据流到也没有实际执行路径」的误报。
具体案例:CodeQL 的污点追踪查询、SonarQube 的安全规则都构建在类似 PDG 的中间表示上。安全团队写一条「从 request.getParameter 到 Statement.execute 的流式查询」,就能在数秒内扫完整仓,发现 SQL 注入与命令注入漏洞。
场景3:大型仓库的测试影响分析
阶段:持续集成与质量保障
解决的问题:大仓每次提交动辄触发上万测试、跑数小时。传统做法是「全量回归」,浪费算力且反馈慢。基于前向切片的影响分析:对本次变更的语句集合求前向切片,只有覆盖到切片内语句的测试才被选中重跑。
具体案例:Google 的测试影响分析平台在每次代码评审后计算影响集,将需要运行的测试裁剪到全量的 1%~10%,让合并前验证从小时级降到分钟级;Meta 的同类平台让万级测试的 monorepo 也能做到「改哪测哪」。
Python 演示
#!/usr/bin/env python3# -*- coding: utf-8 -*-"""小型程序依赖图(PDG)构建与后向切片演示""" class Stmt: """程序语句: 编号, 文本, 写入变量, 读取变量, 控制守卫""" def __init__(self, sid, text, defined, used, ctrl=None): self.sid = sid self.text = text self.defined = set(defined) # 本语句写入的变量 self.used = set(used) # 本语句读取的变量 self.ctrl = ctrl # 控制本语句的 if 条件编号 def data_deps(prog): """扫描定义-使用链, 求数据依赖边(流依赖)""" deps = set() last_def = {} # 变量 -> 最近一次写入它的语句编号 for st in sorted(prog, key=lambda s: s.sid): for v in st.used: # 语句读了哪些变量 if v in last_def: # 存在未覆盖的最近写入者 deps.add((last_def[v], st.sid)) for v in st.defined: # 语句写了哪些变量 last_def[v] = st.sid return deps def all_deps(prog): """合并数据依赖边与控制依赖边, 得到完整 PDG 边集""" deps = data_deps(prog) for st in prog: if st.ctrl is not None: deps.add((st.ctrl, st.sid)) # 控制依赖边: if -> 分支语句 return deps def backward_slice(prog, deps, target): """沿依赖边反向传播, 返回 target 语句的后向切片""" rev = {} for src, dst in deps: rev.setdefault(dst, []).append(src) # 建反向邻接表 worklist = [target] seen = set() while worklist: cur = worklist.pop() if cur in seen: continue seen.add(cur) worklist.extend(rev.get(cur, [])) # 沿反向边继续扩散 return sorted(seen) def main(): prog = [ Stmt(1, "x = a + b", ["x"], ["a", "b"]), Stmt(2, "if (x > 0)", [], ["x"]), Stmt(3, "y = x * 2", ["y"], ["x"], ctrl=2), Stmt(4, "print(y)", [], ["y"], ctrl=2), ] edges = all_deps(prog) print("PDG 依赖边:", sorted(edges)) print("语句3的后向切片:", backward_slice(prog, edges, 3)) print("语句4的后向切片:", backward_slice(prog, edges, 4)) if __name__ == "__main__": main()
self.defined = set(defined)把写入的变量存成集合,后续查「谁是 v 的写入者」只需 O(1)。for v in st.used:逐个检查本语句读取的变量,为它寻找最近的写入者。if v in last_def:撞见「上一个写 v 的语句」还没被覆盖,就说明依赖成立。deps.add((last_def[v], st.sid))记录一条「写入语句 → 读取语句」的流依赖边。deps.add((st.ctrl, st.sid))把 if 条件指向分支语句的控制依赖边也并入边集。rev.setdefault(dst, []).append(src)建反向邻接表,为「倒着走箭头」做准备。worklist.extend(rev.get(cur, []))从切片准则出发,沿反向依赖边一层层扩散收集。
常见误区
误区「控制流图(CFG)画的是执行顺序,程序依赖图(PDG)不就是把每个语句之间『有关联』的都连起来吗?我感觉 CFG 上多画几条箭头就是 PDG 了。」
正解PDG 不是 CFG 的简单加强版。CFG 保留语句的顺序与拓扑,而 PDG 只保留语义依赖关系(数据依赖 + 控制依赖),顺序信息完全分散到依赖边里——同一个 PDG 可以对应多种合法的执行顺序。这就像后厨地图只画「谁给谁供料、谁受谁开关控制」,不画「谁站在谁左边」。把两者混为一谈,做切片时会把不该包含的语句算进来。
为何常见:大多数教材先教 CFG,PDG 又画成带箭头的图,初学者自然以为 PDG 是 CFG 的「超集」。
误区「语句 A 写了变量 x,语句 B 读了变量 x,它们肯定有数据依赖!」
正解数据依赖还要求「中间没有被重新赋值」。如果 A: x=1, B: x=2, C: print(x),那么 C 依赖的是 B 而不是 A,A 与 C 之间没有依赖边。这就像「第一锅汤倒掉了、第二锅汤才是给客人那锅」,第一锅汤和客人的菜没有关系。判断依赖要看最近的写入者(def-use 链),不是单纯看变量名。
为何常见:人对「名字相同」的直觉太强,容易忽略变量的值会被覆盖。
误区「只要写在 if 里面的代码,都一定和控制语句有控制依赖。」
正解控制依赖的定义更严格:只有当「某个分支被执行而另一个分支不执行,导致某语句要么执行要么不执行」时,该语句才控制依赖于这个 if。比如 if (x>0) { y=1; } z=y+1; 中,z=y+1 无论在哪个分支都会执行(它后支配两个分支出口),所以 z 不控制依赖于 if。真正的「开关」只作用于那些会被跳过或选中的语句。
为何常见:从语法嵌套上看「在 if 里面」,而忽略了「无论如何都会执行」的后支配情况。
误区「切片不就是把没用的行删掉嘛,编译器优化里也这么干。」
正解切片是有明确准则的:给定切片准则(某语句 + 一组变量),切片保留的是「会影响该语句在该变量上取值的全部语句」。它删除的语句不是因为「没用」,而是因为不影响切片准则所关心的行为。同一个程序,换一个切片准则,得到完全不同的切片。这跟死代码删除(dead code elimination)的目标和方法都不同。
为何常见:切片前后代码都变短了,看起来都像「删代码」,容易混淆概念。
误区「为了让 AI 更聪明地分析代码,PDG 当然是越精确越好,宁可慢一点也要全量构建。」
正解对百万行级别的大仓库,全量精确 PDG 的边数可到 O(n²),构建时间和内存都无法承受。工业界的做法是权衡:用流不敏感或上下文不敏感的近似,或增量更新只对变更区域重建。精度 90% 但秒级完成,往往比精度 99% 但跑一小时更实用。理论最优和工程可用之间,必须做取舍。
为何常见:学术研究追求精确,初学者容易把「理论上更好」当成「工程上更好」。
面试题
Q1. 程序依赖图(PDG)中的两类依赖边是?
- A.语法依赖边与语义依赖边
- B.数据依赖边与控制依赖边← 正确答案
- C.顺序依赖边与跳转依赖边
- D.调用依赖边与返回依赖边
答案:B。PDG 融合数据依赖(谁给谁供料)和控制依赖(谁受谁开关控制),对应后厨地图里的「原料流动」和「条件闸门」。顺序不是一种独立的依赖边,它被编码进依赖关系中。
Q2. 对于语句 y = x + 1; 与 print(y);,后者对前者的依赖是?
- A.反依赖(读后写)
- B.输出依赖(写后写)
- C.流依赖(写后读)← 正确答案
- D.控制依赖(分支控制)
答案:C。第二个语句读取 y,而 y 是第一个语句写入的,写后读(RAW)构成流依赖,故称「真依赖」。反依赖是读后写,输出依赖是写后写,都不符合这里的读写顺序。
Q3. 关于控制依赖,下列说法正确的是?
- A.凡是写在 if 语句块内的代码都依赖于该 if
- B.语句 j 后支配 i 的所有后继即控制依赖
- C.存在后继 p,j 后支配 p 且不后支配 i← 正确答案
- D.循环中所有语句都控制依赖于其入口
答案:C。控制依赖的经典定义是「存在 i 的一个后继 p,j 后支配 p,但 j 不后支配 i 本身」。选项 A、D 把「语法上在块内或循环内」误当成了依赖关系;选项 B 的条件实际上意味着 j 后支配 i,构不成「开关」。
Q4. 程序切片通常以什么为起始点?
- A.切片准则(语句加变量集)← 正确答案
- B.函数入口语句(第一行)
- C.程序出口语句(最后一行)
- D.任意一条执行路径
答案:A。切片准则 <S, V> 指定「我们关心哪个语句在哪些变量上的取值」。后向切片从这个准则出发,沿依赖边反向收集所有可能影响它的语句。没有准则的「切片」无从谈起。
Q5. 以下哪一项是影响分析最直接的实现手段?
- A.后向切片,从变更语句回推
- B.前向切片,沿依赖边前推← 正确答案
- C.死代码删除,删除变更语句
- D.只计算到达定义数据流
答案:B。变更影响分析关心「改了这一处,还有哪些语句的结果会变」,这正是前向切片:从变更点出发沿依赖边扩散。后向切片用于寻找根源,方向正好相反;死代码删除与影响分析目标无关。
Q6. 数据依赖边存在的必要条件是?
- A.存在未被覆盖的定义-使用链← 正确答案
- B.两语句的物理距离足够近
- C.两语句使用相同类型数据
- D.两语句属于同一个函数
答案:A。变量 v 在语句 i 被定义、在语句 j 被使用,且 i 到 j 的路径上 v 没有被重新定义,才能连出数据依赖边。若中间被覆盖,最近的写入者才是依赖对象——像后厨里被倒掉的第一锅汤。
Q7. PDG 构建中,控制依赖边通常通过哪种结构计算?
- A.借助语法树的前序遍历
- B.后支配树加分支后继分析← 正确答案
- C.依赖符号表的哈希冲突
- D.依赖正则表达式匹配
答案:B。控制依赖定义建立在后支配关系之上。先由 CFG 求后支配树,再对每个节点的每个后继检查「子节点是否后支配该后继但不后支配该节点」,即可生成控制依赖边。
Q8. 判断语句 j 对语句 i 有流依赖,需要满足哪些条件?
- A.i 定义了变量 v← 正确答案
- B.j 使用了变量 v← 正确答案
- C.路径中间没有重新赋值← 正确答案
- D.i 和 j 之间没有控制依赖
答案:ABC。流依赖 = 写后读(RAW),三个条件缺一不可:有定义、有使用、且中间无覆盖。是否同时存在控制依赖并不影响流依赖的存在,D 是干扰项。
Q9. 下列哪些技术直接建立在程序依赖图之上?
- A.程序切片后向与前向← 正确答案
- B.变更影响分析模块← 正确答案
- C.污点分析追踪数据传播← 正确答案
- D.编译器的词法分析器
答案:ABC。切片是 PDG 上的可达性计算,影响分析是前向切片,污点分析本质是沿数据依赖边追踪传播。词法分析只做字符切分,与依赖图无关。
Q10. 大规模程序构建 PDG 时,工程上常采用的取舍包括?
- A.流不敏感近似牺牲精度← 正确答案
- B.增量更新只重建变更区域← 正确答案
- C.压缩合并冗余依赖边← 正确答案
- D.完全放弃控制依赖边
答案:ABC。为控制 O(n²) 的边膨胀,工业界常用流不敏感近似、增量重建、边压缩等手段,在精度与成本间权衡。但控制依赖是 PDG 的半边天,完全放弃会让切片失真,不是常规取舍,D 错误。