你规划了一次环球旅行,行李清单写了满满三页:厚毛衣、两套洗漱包、三本旅行指南、好几个转接头……如果照单全收,箱子重得拖不动,你还得在每个城市之间狼狈地拖着它赶路。幸运的是,出发前你请来了一位经验丰富的旅行规划师。他不动你的行程,只动行李箱——把“确定用不上的”扔掉,把“两件一模一样的”合成一件,把“每天都要掏出来的公交卡”放到外层口袋,再把“又重又厚的旅行指南”换成轻巧的电子书。等行李重新封箱时,行程还是那个行程,东西一样不缺,箱子却轻了一大半。 这位规划师,就是编译器里的“代码优化器”。程序员写下的源码像一张任性的行李清单;机器码像行李箱。编译器优化的全部艺术,就是在“不改变程序行为”这条铁律之下,把代码收拾得又快又轻。本文沿着这条旅行主线,逐个拆解常量折叠与传播、死代码消除、公共子表达式消除、复写传播、循环不变量外提、强度削弱六大门派,最后看看 LLVM 这个“金牌规划团队”如何把零散技巧变成工业化流水线。
对 AI 从业者而言,代码优化是理解“为什么同一个模型在不同推理引擎里速度差几倍”的钥匙,也是通往编译器后端与高性能系统工程师岗位的必修技能。
目录
可视化演示
展示编译器局部优化(常量折叠/死代码消除/公共子表达式)与全局优化(复写传播/循环不变量外提/强度削弱)技术原理及优化流水线效果。切换模式查看各维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
局部优化技术 常量折叠(constant folding):编译期计算常量表达式(如3+4→7),常量传播将已知常量值替换变量引用,消除运行时计算 死代码消除(DCE):删除不可达代码和结果未使用代码;公共子表达式消除(CSE):识别重复计算用临时变量替换,减少冗余运算 local_opt = const_fold + const_prop + DCE + CSE + copy_prop 局部优化作用于基本块内
优化概述:旅行规划师的第一守则
你决定去环球旅行,行李清单写了满满三页。出发前请来一位资深旅行规划师,他接过清单,说的第一句话不是“我带您精简”,而是“您的行程单一个字不许改,我只动行李箱”。这句话,就是整个旅行合作的地基。
这句承诺,对应编译器优化的立身之本——语义保持(semantics preservation)。优化器可以对代码做各种手脚:把计算挪位置、把重复消除、把昂贵的运算换成便宜的,但程序对外表现必须与优化前一模一样。生活里这叫“行程不能变”,编译器圈这叫“不改变程序的可观察行为”。
代码优化按作用范围分两类:局部优化只在一个基本块(一段顺序执行的指令序列)内进行,视野小但分析快;全局优化跨越多个基本块,需要数据流分析(data-flow analysis)——沿着程序的“信息流”追踪变量的来龙去脉。现代编译器(如 LLVM)大多在中间表示(IR)层做优化:IR 既保留控制流结构,又贴近机器指令,是优化战役的主战场。
守住“行程一个字不改”的底线,规划师才敢大刀阔斧地扔东西、换东西。编译器优化师也一样——只要等价性在手,减法、换装、挪位,都可以尽情发挥。
- P_src(源程序)优化开始前的原始程序,相当于那张三页的行李清单。
- P_opt(优化后程序)优化器输出的最终程序,相当于规划师重新封箱后的行李箱。
- ≡(语义等价)两个程序对任意输入都产生相同的可观察结果,这是优化的底线承诺。
常量折叠与传播:出发前查好的小本子
规划师出发前做的第一件事,是把汇率、天气、时差这些“固定信息”查清楚,写进一本小本子。旅途中凡是遇到需要换算的价格,直接翻本子,从不现场掏出手机现算。
编译器里也有这么一类“固定信息”:常量。常量折叠(constant folding)是指把编译期就能算出来的表达式直接算完,比如 3×4+2 直接变成 14;常量传播(constant propagation)是指把已知常量的值沿着程序路径向前带,凡是用到该变量的地方都换成这个值。两者最常见的配合套路是“先传播,后折叠”——先把常量值送到使用点,折叠的触发面就会更大。
触发条件并不苛刻:折叠要求表达式所有操作数都是编译期已知的常量;传播要求赋值语句右侧是常量,且路径上该变量没有被改写。常量折叠看起来是小打小闹,却是后续大优化的“燃料”——程序里常数一多,死代码消除和强度削弱才轮得上出手。
小本子写满之后,旅途中的“临时心算”就消失了。常量一旦被“钉死”在代码里,后续优化器就能放心地对变量做更多手术。
- 子表达式 3×4两个操作数都是编译期常量,满足折叠触发条件,先算掉。
- 折半结果 12+2生成 12 后继续参与外层表达式折叠,一步步推进。
- 最终 x=14运行时代码直接装载常量 14,两条算术指令从此消失。
死代码消除:扔掉那件厚毛衣
规划师把行李箱摊开,看到那件厚毛衣便笑了——全程 30°C 的高温海岛行,它永远不会被穿上,却占掉整整一层。果断拿出去。
编译器里的“厚毛衣”叫死代码(dead code):要么是永远执行不到的代码(return 之后的语句、if(false) 分支),要么是“算了但没人用”的赋值。后者叫冗余赋值(dead store),删除它需要一份关键情报:变量在某个程序点之后,到底还会不会被读取?
这份情报来自活跃变量分析(live variable analysis)。一个变量在某点“活跃”,指从该点出发沿某条路径、在它被重新赋值之前还会被读取。基于活跃信息,编译器从后往前扫描指令:某条赋值的结果在之后从未被读取,它就是死代码,可以安全删除。而且删除会引发连锁反应——被删指令中引用的变量可能因此变成死变量,继续删,直到收敛稳定。
规划师扔掉的只是“确定用不上”的厚毛衣。同理,死代码消除只删“确定不会被读取”的赋值与“永远到不了”的语句——多扔一件,箱子就轻一分。
- Use[B]基本块 B 内部“先读取后改写”的变量集合,像“这一站真正要用到的物品”。
- Def[B]在 B 内部被重新赋值的变量集合,像“这一站被换掉的旧物”。
- LIVE_out[B](流出活性)从 B 出口流出去之后仍被后续代码需要的变量集合。
- LIVE_in[B](流入活性)块内直接用到、加上从后面传回来且未被本块改写的,构成流入时必须活跃的集合。
公共子表达式消除:一个充电器就够了
箱子里有两件一模一样的 USB-C 充电器。规划师拎起来看了看:两个充电器功能完全相同,带一个就行,另一个需要时直接借用第一个。
代码里的“一个充电器”,就是公共子表达式消除(Common Subexpression Elimination,CSE)。如果一个表达式 b×c 在之前已经被计算过,且两次计算之间 b、c 都没有被改写,那么第二次出现时就不必重新算,直接取第一次的结果。实现上通常引入一个临时变量 t 保存 b×c 的值,然后把第二处替换成对 t 的引用。
判断“结果是否仍然有效”,依赖可用表达式分析(available expressions):一个表达式在某程序点“可用”,指它在之前已被计算过,且结果没有被后续赋值破坏。CSE 的收益直观地体现为减少算术指令;但要注意,如果表达式涉及内存访问,CSE 可能改变访存次数与顺序,编译器需要更加谨慎。
同一个充电器只占一份重量。CSE 让代码里的“重复劳动”只做一次,并把第一次的结果像充电器一样共享给所有需要的地方。
- 第一次计算 b×c结果存入 a,同时编译器记录“这个表达式已经算过”。
- 第二次出现 b×c检查两次计算之间 b、c 未被改写,说明表达式仍然可用。
- 复用结果用 a 替代第二次的 b×c,乘法少做一次。
复写传播:免去中间倒腾
行程里有这么一步:先把行李存到寄存处,再绕回去取,然后才出发。规划师看后扶额——直接带着行李出发不就行了?“放 A 再挪 B”的中间环节纯属折腾。
代码里的“寄存处”是复写语句(copy statement),形如 x = y——把一个变量的值复制给另一个变量。复写传播(copy propagation)做的事很简单:如果 x 在被 y 赋值之后一直没有被改写,那么所有读到 x 的地方都可以直接用 y 替换,从而省掉那次复制。
有趣的是,复写传播单独运行往往省不了几个钱,它真正的价值在于“拆桥”:x 的引用被替换后,x = y 这条赋值就变成了死代码,可以被 DCE 顺手清掉;若两个表达式原本分别引用 x 和 y,经传播统一成同一个变量后变得相同,又为 CSE 创造了机会。这正是编译器优化的链条之美。
规划师把“存寄存处再取回”改成直接带身上,省掉一段空跑。复写传播单独看不赚钱,却能引爆死代码消除与 CSE 的连锁收益——高手优化从来是一环扣一环。
- 复写语句 x=y编译器登记“x 是 y 的影子”,两者当前值相同。
- 读取点 z=x+1x 在赋值后未被改写,用 y 替换 x 是安全的。
- 改写结果z 直接使用 y,省掉一次复制;x=y 随后沦为死代码。
循环不变量外提:公交卡放外层口袋
旅途中每天早晚都要刷公交卡。按原始打包方式,卡埋在行李箱最底层,每次刷卡都要开箱、翻底、取卡、再放回——一天十几次,腰都累断。规划师把卡放进外套口袋,从包里只“掏”一次。
程序里的“反复开箱”就是循环。循环体可能执行成千上万次,如果其中某条语句的计算结果每一轮都相同——操作数在循环内没有被改写——它就叫循环不变量(loop invariant)。每轮都重复算它,纯属浪费。循环不变量外提(LICM)把它搬到循环之前,只执行一次。
外提有严格的前置条件,保证语义不变:第一,该语句在循环内所有路径上都会执行,不能在某次迭代中被跳过;第二,循环内没有其他语句改写它引用的变量;第三,循环若存在多个出口,移出后不能改变“到达某些出口时才执行”的语义。满足这些条件后,收益就是每轮代价乘以省掉的迭代次数。
公交卡只在出发前装进口袋,之后每次刷卡都不再翻箱倒柜。循环不变量外提是收益最大的优化之一——哪怕单次计算很便宜,乘以千万次迭代也是巨额财富。
- 单轮代价 C该语句在循环体内每执行一次的开销(时间或资源)。
- 迭代次数 N循环体执行的轮数,N 越大,外提的收益越可观。
- 省掉的次数 N−1外提后第一次在循环前完成,循环内从第 1 轮起不再重复。
- 总收益单轮代价乘以省掉的次数,这就是外提带来的净节省。
强度削弱:实体书换 Kindle
规划师又翻开书单:三大本旅行指南,每本两公斤。他建议换成 Kindle 里的电子版——阅读体验几乎不变,重量从六公斤骤降到三百克。
编译器里的“实体书”是昂贵的运算。强度削弱(strength reduction)用代价更低的等价运算替换高代价运算,最经典的场景发生在循环中:i 是归纳变量(induction variable)——每轮迭代固定 +1,而 j = 4×i 每轮都要重做一次乘法。既然 i 每轮固定加 1,j 每轮就固定加 4,何不把乘法换成累加?
变换时要注意初值与退出条件的配合:进入循环前,j 要初始化为 4 乘以 i 的初值;循环结束时,原本由 i 派生出的 j 值也要照常维护。强度削弱常与循环不变量外提、归纳变量消除配合使用,是现代编译器循环优化家族的顶梁柱。
旅行指南从“六公斤实体书”变成“三百克电子书”,旅行体验完全一样。强度削弱就是用加法替代乘法这类“更便宜的等价物”——循环跑得越多,省下的力气越可观。
- 原始形态每轮迭代都用 i 重新计算 j,乘法代价高。
- 增量分析i 每轮 +1,因此 j 每轮的增量固定为 4。
- 改写后循环体内只剩加法,乘法只出现在循环前的初始化。
LLVM实践与收益评估:标准清单与体重秤
作为业内人士,规划师从不凭感觉行事。他有一份标准化检查清单:查固定信息、扔无用物、合并重复、免去倒腾、外提不变项、替换重物。每做完一项,他还要过磅,看看行李箱到底轻了多少——没有称重的优化都是耍流氓。
LLVM 的优化器就是这份“清单机器”。每个优化动作被封装成一个 Pass(优化遍),命令行工具 opt 按固定顺序调度它们:-O1 做基础清理,-O2 叠加循环优化与全局标量优化,-O3 再加向量化与更激进的变换。LLVM IR 采用 SSA 形式(静态单赋值,每个变量只赋值一次),让数据流分析变得干净利落,也为上述各类优化提供了坚实的地基。
收益评估同样讲究:每个 Pass 都有自己的代价模型。收益来自减少指令数、访存与分支;成本来自分析耗时、寄存器压力与代码体积膨胀。工程师用加速比衡量单个 Pass 或整条优化链的价值,并在一组基准程序(benchmark)上取几何平均做综合判断,防止个别样本虚高。
规划师用体重秤判断每一招是否有效,LLVM 用加速比衡量每个 Pass 的价值。只有“称过”的优化才值得保留——这正是工程直觉与理论分析的结合点。
- T_before优化前程序的总执行时间,作为度量基准。
- T_after优化后程序的总执行时间。
- 加速比 S两者相除,S>1 说明优化有正向收益,S 越大越快。
AI 应用场景
ML 编译器与推理引擎的算子优化
阶段:模型部署与推理加速
解决的问题:训练好的模型在推理时,算子计算图里往往充斥着重复张量运算、循环内的不变计算和高代价的逐元素变换,导致延迟居高不下。传统手写内核耗时费力,而编译器可以在 IR 层自动完成这些“旅行打包”。
具体案例:XLA 在把 TensorFlow 计算图编译为 GPU 内核时,会应用公共子表达式消除来消除重复张量运算,用循环不变量外提把 padding 等不变计算移出循环;TVM 的调度模板中,工程师也会显式或自动地套用强度削弱,把推理延迟降低数倍。
JIT 加速数据预处理管道
阶段:训练数据预处理
解决的问题:Python 编写的数据预处理循环通常很慢,原因之一是每轮迭代重复计算不变的缩放系数、做了大量多余赋值。JIT 编译器在运行时把热路径编译成机器码,自动完成死代码消除与强度削弱。
具体案例:用 Numba 的 nopython 模式编译图像归一化循环时,编译器会自动把循环内不变的均值、方差外提,把除法换成乘法,预处理耗时下降 3 到 5 倍,且代码逻辑一字未改——就像规划师只动了行李箱。
Python 演示
# -*- coding: utf-8 -*-# Mini three-address-code optimizer demo:# constant propagation/folding, copy propagation, CSE,# dead code elimination, loop invariant code motion. def constant_propagation(ir): """Propagate constant values along straight-line code.""" vals, out = {}, [] for d, o, a, b in ir: if o == '=' and isinstance(a, int): vals[d] = a else: vals.pop(d, None) a, b = vals.get(a, a), vals.get(b, b) out.append((d, o, a, b)) return out def constant_folding(ir): """Fold expressions whose operands are all literals.""" out = [] for d, o, a, b in ir: if isinstance(a, int) and isinstance(b, int): if o == '+': o, a, b = '=', a + b, None elif o == '*': o, a, b = '=', a * b, None out.append((d, o, a, b)) return out def copy_propagation(ir): """Replace uses of x with y after a copy 'x = y'.""" copies, out = {}, [] for d, o, a, b in ir: if o == '=' and isinstance(a, str): copies[d] = a else: copies.pop(d, None) a, b = copies.get(a, a), copies.get(b, b) out.append((d, o, a, b)) return out def cse(ir): """Compute an expression once and reuse its result.""" seen, out = {}, [] for d, o, a, b in ir: if o in ('+', '-', '*'): key = (o, a, b) if key in seen: out.append((d, '=', seen[key], None)) else: seen[key] = d out.append((d, o, a, b)) else: out.append((d, o, a, b)) return out def dead_code_elimination(ir): """Delete assignments whose result is never read.""" used, alive = set(), [] for d, o, a, b in reversed(ir): if o == '=' and d not in used: continue alive.append((d, o, a, b)) if isinstance(a, str): used.add(a) if isinstance(b, str): used.add(b) return list(reversed(alive)) def loop_invariant_code_motion(body): """Hoist statements whose operands are never defined in the loop.""" defs = {d for d, o, a, b in body if o != 'param'} hoisted, kept = [], [] for ins in body: d, o, a, b = ins (hoisted if a not in defs and b not in defs else kept).append(ins) return hoisted, kept def dump(tag, ir): print(tag) for d, o, a, b in ir: rhs = 'param' if o == 'param' else ('= ' + str(a) if b is None else o + ' ' + str(a) + ' ' + str(b)) print(' %-4s%s' % (d + ':', rhs)) def main(): ir = [ ('a', 'param', None, None), ('b', 'param', None, None), ('m', '=', 3, None), ('n', '=', 4, None), ('c', '*', 'a', 'b'), ('d', '=', 'c', None), ('e', '+', 'd', 1), ('f', '*', 'a', 'b'), ('g', '=', 99, None), ('h', '+', 'f', 'e'), ('k', '*', 'm', 'n'), ('out', '+', 'h', 'k'), ] dump('=== original IR ===', ir) ir = constant_propagation(ir); dump('--- after const-prop ---', ir) ir = constant_folding(ir); dump('--- after folding ---', ir) ir = copy_propagation(ir); dump('--- after copy-prop ---', ir) ir = cse(ir); dump('--- after CSE ---', ir) ir = dead_code_elimination(ir); dump('--- after DCE ---', ir) loop = [('i', 'param', None, None), ('n', 'param', None, None), ('t', '*', 'n', 4), ('r', '+', 't', 'i')] hoisted, kept = loop_invariant_code_motion(loop) dump('=== hoisted out of loop ===', hoisted) dump('=== kept in loop ===', kept) if __name__ == '__main__': main()
a, b = vals.get(a, a), vals.get(b, b)查常量表,把能替换的引用换成已知常量值。if o == '=' and isinstance(a, int): vals[d] = a常量赋值入表,作为后续传播的依据。if isinstance(a, int) and isinstance(b, int):折叠的触发条件:操作数全是编译期常量。copies[d] = a登记复写关系,x 是 y 的“影子”。a, b = copies.get(a, a), copies.get(b, b)把读取 x 的地方替换成 y,完成复写传播。key = (o, a, b)用“运算 + 操作数”作为表达式签名,识别公共子表达式。if key in seen: out.append((d, '=', seen[key], None))命中签名则复用第一次的结果。if o == '=' and d not in used: continue反向扫描时,结果没人读的赋值直接丢弃。
常见误区
误区听说编译器优化很“聪明”,会不会把程序逻辑改坏?我总觉得开 -O2 心里不踏实,宁可关掉优化,反正功能对就行。
正解语义保持是优化的铁律,就像旅行规划师承诺“行程一个字不改”。优化器能被允许改变行为的前提,是源代码本身触发了未定义行为(如整数溢出、悬垂指针)——那是程序员的锅,不是优化的锅。浮点运算的“结果顺序不同”属于精度范畴,通常由单独的运算模型控制,与乱改逻辑是两回事。
为何常见:网上大量“优化导致 bug”的帖子,绝大多数最终定位到代码自身存在未定义行为,初学者容易把因果记到优化器头上。
误区我看到代码里有个变量声明后一直没用到,手动删掉,这应该就是编译器做的“死代码消除”吧?
正解源码层的“删没用的声明”只是清理表面垃圾。编译器里的 DCE 是在 IR 上做数据流分析:它根据活跃变量信息,从后往前扫描,删除“执行了但结果从不被读取”的赋值,还会连锁删除由此变死的指令。它不关心变量名好不好看,只关心“有没有人读结果”。
为何常见:初学者从“垃圾代码”的直觉出发理解优化,容易忽略背后的活跃变量分析才是 DCE 的灵魂。
误区公共子表达式消除?不就是看到两行 `a = x * y` 和 `b = x * y`,删掉一行,让 b 用 a 吗?
正解关键在于“两次计算之间 x、y 有没有被改写”。如果中间插入了 x = 5,第二次的 x * y 就不再等于第一次的结果,删不得。真正安全的 CSE 需要可用表达式分析做背书,并且通常引入临时变量保存中间结果;涉及内存读写的表达式还要考虑访存副作用,远非“删一行”那么简单。
为何常见:源码层面的“肉眼重构”经验,和编译器在控制流图上做全局安全分析,是两种不同量级的工程。
误区编译器不开到最高优化等级就是浪费,无脑 `-O3`,反正优化越多越快。
正解强度削弱、循环展开等激进优化会增加代码体积与寄存器压力,可能引发指令缓存失效、寄存器溢出,反而变慢。实践上 -O3 相比 -O2 的收益通常有限,甚至为负。正确姿势是用基准测试(benchmark)实测,让“体重秤”说话,而不是拍脑袋。
为何常见:把“优化”想成免费的提速按钮,忽略了代价模型与硬件特性的复杂交互。
误区循环里只要有一条语句的值看起来不变,比如 `x = n * 4`,直接把它提到循环前面不就行了?
正解外提必须满足三条前置条件:语句在循环内所有路径上都执行、循环内不改写其操作数、移出后不改变循环出口处的语义。若语句本身依赖某次循环迭代的条件才执行,贸然外提会改变执行时机,甚至改变程序结果。规划师把公交卡放外层口袋的前提是“每天都要用”,可不是随便什么东西都往外掏。
为何常见:只盯着收益公式 C×(N−1) 心动,忽略了安全性分析才是优化的第一课。
面试题
Q1. 编译优化的根本原则是什么?
- A.让代码行数变少
- B.保持语义不变的前提下提升效率← 正确答案
- C.让源码更好看
- D.减少程序员的工作量
答案:B。优化的一切动作都以语义保持为铁律,相当于旅行规划师“只动行李箱、不改行程单”。行数变少只是可能出现的副产物,不是目标本身。
Q2. 常量折叠适合处理以下哪种情况?
- A.操作数均为编译期常量的表达式← 正确答案
- B.循环条件不变的分支
- C.函数参数每次调用都不同
- D.复写语句 x = y
答案:A。折叠要求所有操作数都是编译期已知的常量,例如 3×4+2 直接算出 14。函数参数运行时才确定,无法折叠;复写语句走的是复写传播。
Q3. 在 LLVM 中,一个具体的优化步骤通常被封装为什么?
- A.Pass← 正确答案
- B.Loop
- C.Block
- D.Type
答案:A。LLVM 把常量折叠、DCE、LICM 等每个优化动作封装成独立的 Pass,由 opt 工具按顺序调度,就像规划师清单上的一个个检查项。
Q4. 关于死代码消除,下列说法正确的是?
- A.只删除注释和无用的 import
- B.删除结果从不被读取的赋值语句← 正确答案
- C.必须依赖运行时探测才能完成
- D.与活跃变量分析没有关系
答案:B。DCE 的核心是活跃变量分析:从后往前扫描,赋值结果在后续从未被读取即为死代码。它是编译期的静态分析,不需要运行时探测。
Q5. 循环不变量外提的安全前置条件包括哪些?
- A.语句在循环内所有路径上都会执行← 正确答案
- B.循环内没有其他语句改写它引用的操作数← 正确答案
- C.循环迭代次数必须是素数
- D.移出后不影响循环出口处的语义← 正确答案
答案:ABD。外提相当于把公交卡放到外套口袋——前提是“每天都要用”(所有路径执行)、“不会被调包”(操作数不被改写)、“出门顺序不变”(出口语义一致)。迭代次数与素数无关。
Q6. 公共子表达式消除成立的前提是两次计算之间?
- A.操作数没有被改写← 正确答案
- B.操作数必须是常量
- C.两次计算必须跨函数
- D.代码行数必须小于 10
答案:A。CSE 要求两次相同表达式之间,其操作数未被改写,这样第一次结果仍然有效。操作数是变量完全可以,只要它们的值没变;跨函数与行数都不是条件。
Q7. 复写传播能为其他优化带来哪些间接收益?
- A.提升源代码的可读性
- B.使中间赋值语句变成死代码← 正确答案
- C.为公共子表达式消除创造条件← 正确答案
- D.把常量变成变量
答案:BC。复写传播把 x 的引用换成 y 后,x = y 本身没人读了,DCE 可以删掉它;表达式被统一引用后也更可能被 CSE 识别。它属于“拆桥型”优化,收益往往要借助队友兑现。
Q8. 强度削弱中,循环内 j = 4 * i(i 每轮 +1)通常被改写成什么?
- A.
j = j + 4← 正确答案 - B.
j = i + 4 - C.
j = 4 * 4 - D.
j = j * 4
答案:A。i 每轮固定 +1,j 的增量就固定为 4,循环体内用加法累加即可,乘法只保留在循环前的初始化。这就是“实体书换 Kindle”的典型操作。
Q9. 下列哪些优化依赖数据流分析?
- A.活跃变量分析指导死代码消除← 正确答案
- B.可用表达式分析指导公共子表达式消除← 正确答案
- C.常量传播沿着路径传递常量值← 正确答案
- D.删除代码中的注释
答案:ABC。这三者都沿着控制流图追踪“信息流”——变量活不活跃、表达式可不可用、常量值到不到达,全是数据流分析的标准问题。删注释只是文本处理,与数据流无关。
Q10. 加速比 S = T_before / T_after 的含义是什么?
- A.S > 1 表示优化带来正向收益← 正确答案
- B.S = 0 表示程序达到最优
- C.S < 1 表示程序被加速
- D.S 越大说明代码越短
答案:A。加速比是优化前后耗时的比值,大于 1 说明优化后更快。S 衡量的是时间收益而非代码体积,S < 1 反而意味着优化拖慢了程序。