5.2 局部优化:基本块内的手术


5.2 局部优化:基本块内的手术

本节摘要:基本块是只能从顶部进入、从底部离开的极大连续指令序列,是局部优化的手术台。本节学习基本块划分规则、流图构造,然后在块内做三台经典手术:DAG 上的公共子表达式消除、常量折叠与常量传播、死代码删除——主线语句的四条三地址码将当场瘦成一条。

阅读完本节,你应当能够:

  1. 按首领指令规则划分基本块并连出流图
  2. 在基本块的 DAG 上做公共子表达式消除
  3. 执行常量折叠与常量传播,写出每步结果
  4. 用活跃性判定死代码并安全删除

基本块:切分与连线

划分规则三条,找"首领指令":跳转目标、紧跟跳转之后的指令、函数入口。首领到下一首领之前(不含)就是基本块。

中间代码片段(含控制流): B1: t1 = cvt_float(qty) ← 函数入口,首领 t2 = price * t1 if t2 < 100 goto L1 ← 跳转,块在此结束 B2: t3 = t2 - discount ← 紧跟跳转,首领 total = t3 goto L2 B3: L1: total = 0 ← 跳转目标 L1,首领 B4: L2: return total ← 跳转目标 L2,首领 流图边:B1→B2(顺序)、B1→B3(条件真)、B2→B4、B3→B4

块内的优良性质:指令逐条顺序执行到底,无中途回头——这保证"块内某变量的值"讨论起来简单,局部优化的所有安全性论证都建立在这条性质上。

手术一:DAG 与公共子表达式消除

基本块的 DAG(有向无环图)把相同计算的节点合并。构造规则:遇到 x = y op z,先查是否已有"op 作用于 y、z 同一取值"的节点,有则复用,无则新建。看一个双份计算的例子:

优化前: a = b * c d = b * c ← 与第一条完全同源(b、c 都没变过) e = a + d 构造 DAG: 第1条:新建节点 n1 = 乘(b, c),a 挂在 n1 上 第2条:查得乘(b, c) 已存在(n1),d 也挂到 n1 —— 不新建 第3条:新建 n2 = 加(n1, n1),e 挂上 从 DAG 重建代码(公共子表达式已消除): a = b * c d = a ← 复用结果,复制代替重算 e = a + d

注意安全边界只在块内成立:若两条 b * c 分居两个块,中间某块改了 b,就不能合并——跨块合并要靠 5.4 节的可用表达式分析背书。

手术二:常量折叠与传播

折叠是"编译器替你算":操作数都是编译期已知常量,直接算出结果写死。传播是"把已知值往前送":变量已被常量赋值,后续引用替换为常量,又为下一轮折叠创造条件。对主线语句动手(设 qty 与 discount 已知):

已知:qty = 2(int 常量)、discount = 0.0(float 常量) 第1轮 传播(用已知常量替换引用): t1 = cvt_float(2) ← qty 换成 2 t2 = price * t1 t3 = t2 - 0.0 total = t3 第2轮 折叠(编译期可算的算掉): t1 = 2.0 ← int 2 转浮点,编译器可折 t2 = price * 2.0 ← price 非常量,乘法保留 t3 = t2 - 0.0 → t2 ← 减 0.0 折成恒等,t3 与 t2 同值 total = t2 第3轮 强度削弱顺手(乘 2.0 换加法): t2 = price + price total = t2

浮点的谨慎例外值得一提:严格浮点模式下 x - 0.0 对负零的处理可能与恒等不等价,编译器默认遵守模式开关——安全第一,语义红线不越。

手术三:死代码删除

一条指令死后,删除它不影响任何可观察行为。判据用活跃性:若变量的值在本次定值之后(沿一切可能路径)不再被任何计算或输出使用,该定值就是死的。块内判定简单:从块尾倒扫,未被引用的定值即死。

第3轮后的块(倒扫过程): t2 = price + price total = t2 倒扫:total 在块尾被后续使用吗?——假定本块之后有人读 total,保留 t2 在 total = t2 之后还有引用吗?——无,t2 死 消除 t2:total = price + price 四条,最终一条。t1、t2、t3 三次死亡证明: t1 死于折叠(值并入指令)、t3 死于恒等折叠、t2 死于复写消除

三台手术的顺序也有讲究:传播喂折叠、折叠喂死代码删除,一环扣一环。真实编译器把这类"局部手术包"做成一个 pass,单块进出、收益稳定,是最便宜的优化。

图 四条指令的瘦身全程

图 四条指令的瘦身全程

⚠️ 常见坑:在开优化档位下调试程序,断点行为"诡异"——变量被折叠进表达式、语句被删、执行行号跳跃。这不是编译器有毛病,是 O0 存在的全部理由:调试时关优化,让源代码与指令一一对应。

💡 关键直觉:局部优化的三条安全论证全靠"块内顺序执行、无回头"这条性质,所以它们便宜;跨块的世界(下一节之后的领域)每条结论都要拿数据流分析换。视野的价格差,是优化分类学的底层逻辑。

手术台边的追问

问:公共子表达式消除有没有反例? 有。两个相同的浮点表达式,若编译器不能证明操作数未变(涉及函数调用、指针解引用),必须保守放弃;即便能证明,浮点结果逐位一致才可合并——带非严格浮点模式的表达式重排可能改变末位。整数世界干净利落的优化,到浮点世界处处要出示证明。

问:常量传播会不会传播过头? 会越界的是人不是算法:跨文件传播需要全程序信息(第 8 章的过程间分析),单看一个函数时,全局变量被本函数外的调用修改的可能无法排除,传播只能停在边界。传播范围等于分析视野,这条等式在本节、下一章、第 8 章反复出现。

补一问:死代码和不可达代码是一回事吗? 不是。死代码是计算了没人用的值(活跃性判定),指令执行但结果被丢弃;不可达代码是控制流根本到不了的位置(跳转分析判定),指令根本不执行。前者删的是无用功,后者删的是幽灵。两者的检测手段、删除依据都不同,混用术语是优化讨论里最常见的口误。

本节要点回顾

  • 基本块划分:跳转目标与跳转后继是首领,流图按控制流连线
  • DAG 消除:同源计算挂同一节点,重建代码即消除公共子表达式
  • 传播与折叠:常量替换喂编译期计算,浮点恒等变换受模式约束
  • 死代码删除:块内倒扫查活跃性,无人引用的定值安全删除
  • 手术接力:传播喂折叠、折叠喂删除,单 pass 打包最划算

块内手术做完,下一节把主线语句放进循环——那里是优化回报最高的手术台。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U