Archipelago

Lec 7程序分析与优化(基本块内)

讲义完成更新于 2026-09-16

Lec 07 程序分析与优化(基本块内)

参考:本讲聚焦基本块内 (intra-basic-block) 的分析与变换,是 L8 全过程数据流分析的基础

TL;DR

  • 基本块内没有控制流汇合,可以用符号执行跟踪值号、常量、拷贝和最后一次使用。
  • 值编号 / CSE、拷贝传播、常量传播和死代码消除会相互创造新机会,通常需要按组合反复执行。
  • 代数化简不是无条件的等式改写;浮点、溢出、除零和副作用都会改变「看似等价」变换的合法性。

1. 程序分析与变换(Analysis & Transformation)

定义(程序分析)

程序分析是编译期对程序运行期行为的推理。可发现总是为真的事实("语句 y=x+z 中 x 恒为 1"、"return 5 永不执行"),也可推断很可能为真的事实("引用 r 通常指向 C 类对象")。需区分数据属性与控制流属性。

变换 (transformation) 用分析结果改写程序,目标:减少执行指令数、减小代码体积、减少周期数(用向量/DSP 指令、改善 cache 命中)、降功耗、省内存。


2. 基本块与符号执行(Basic Blocks & Symbolic Execution)

基本块定义同 L6(极大、无中途跳入/跳出)。基本块分析方法:假设规范化基本块——所有语句形如 var = var op var、var = op var、var = var;然后符号化模拟执行 (symbolic execution) 基本块,推理变量的值(或其他计算属性),导出感兴趣的性质。

2.1 两类变量

定义(临时变量 vs . 程序变量)

  • 编译器引入的临时变量 (temporaries):只在基本块内传值,由指令扁平化/优化引入,通常只被赋值一次。

  • 程序变量 (program variables):源程序声明,可多次赋值,可在基本块间传值。 :::

2.2 基本块优化菜单

公共子表达式消除  a=(x+y)+z; b=x+y;   ⟹  t=x+y; a=t+z; b=t;
常量传播          x=5; b=x+y;         ⟹  x=5; b=5+y;
代数恒等          a=x*1;              ⟹  a=x;
复制传播          a=x+y; b=a; c=b+z;  ⟹  a=x+y; b=a; c=a+z;
死代码消除        a=x+y; b=a; b=a+z;  ⟹  a=x+y; b=a+z;
强度削弱          t=i*4;              ⟹  t=i<<2;

3. 值编号与公共子表达式消除(Value Numbering & CSE)

前向符号执行基本块,给每个变量与表达式分配一个虚拟值 (virtual value);发现的性质是"哪些变量/表达式有相同的值"。标准用途是 CSE:把算出的值存进临时变量,当某表达式的值之前已算过时,用临时变量替换之。

维护三张映射:

  • Var→Val:每个变量的符号值(处理 x=y+z 时用它求 y、z 的值);
  • Exp→Val:每个已求值表达式的值(当 value(y)+value(z) 已算过时,用来更新 Var→Val);
  • Exp→Tmp:持有每个已求值表达式的临时变量(处理 x=y+z 时决定复用哪个 tmp)。

每条新值赋给一个临时变量:a=x+y; 变成 a=x+y; t=a;——即使原变量后续被改写,临时变量也保住了该值供后用。

例题(值编号的三个有趣性质)

① 即使用不同变量也能发现公共子表达式(因用符号值):y=a+b; x=b; z=a+x; ⟹ y=a+b; t=y; x=b; z=t;

② 即使原变量被覆盖也能发现(因值已存入 tmp):y=a+b; y=1; z=a+b; ⟹ y=a+b; t=y; y=1; z=t;

③ 扁平化 + CSE 可捕获任意复杂的部分公共子表达式:w=(a+b)+c; x=b; y=(a+x)+c; z=a+b; 扁平化后 t1=a+b; w=t1+c; x=b; t2=a+x; y=t2+c; z=a+b;,CSE 发现 t1+c 与 t2+c 同值、z=a+b 已算过,可全部复用。

两个遗留问题:(I) 引入大量临时变量与拷贝语句(用复制传播 + 死代码消除清理);(II) 表达式必须完全相同才识别(用规范化 canonicalization + 代数化简处理 a+b+c vs b+c+a)。


4. 复制传播(Copy Propagation)

定义(复制传播)

符号模拟执行,尽量用原变量替代临时变量。关键:判断原变量在"其赋值语句"与"使用算出值"之间是否被覆盖;若未被覆盖,则用原变量。

维护两张映射:tmp→var(某临时变量该用哪个变量替代)与 var→set(其逆,某变量被哪些 tmp 映射)。算法:每条语句,若 RHS 中临时变量在 tmp→var 则替换为 var;若 LHS 变量在 var→set,则把该集合里的 tmp 在 tmp→var 中失效(如 a=b 重定义 a 后,t1→a 改为 t1→t1、a→{})。

CSE + CP + DCE 协同:a=x+y; b=x+y; → CSE:a=x+y; t=a; b=t; → CP:a=x+y; t=a; b=a; → DCE:a=x+y; b=a;


5. 死代码消除(Dead Code Elimination, DCE)

定义(死代码消除,逆向)

逆执行顺序处理代码,维护一个"后续计算所需变量"的集合 (needed set);遇到对"不被需要的"变量的赋值,删除该赋值。

例题(DCE 逆向扫描)

初始 needed = {b}(假设 b 后续需要)。逆序处理:

a=x+y; t1=a; b=a+z; t2=b; c=a; a=b;     needed={b}
↑ a=b:a∈?否→… 实际从末尾起,逐步把 RHS 变量加入、删除无用赋值
最终:a=x+y; b=a+z; c=a; a=b;            (t1、t2 被删)

末态 needed={x,y,z}。


6. 代数化简(Algebraic Simplification)

用代数/数论知识化简表达式:

a+0 ⟹ a      a*1 ⟹ a      a/1 ⟹ a      a*0 ⟹ 0      0-a ⟹ -a
a+(-b) ⟹ a-b   -(-a) ⟹ a
a∧true ⟹ a    a∧false ⟹ false   a∨true ⟹ true   a∨false ⟹ a
a^2 ⟹ a*a    a*2 ⟹ a+a    a*8 ⟹ a<<3

机会来源:程序员懒得化简(可读性优先);编译器展开后(如 A[8][12] 展开为 *(Abase+4*(12+8*256)));其他优化之后。

定理(代数化简的实现要点)

  • 不是数据流优化:在表达式树上匹配化简规则并改写,候选未必显然(如 a+b-a)。

  • 利用运算性质:交换律 a op b=b op aa\,op\,b = b\,op\,a、结合律 (a op b) op c=b op (a op c)(a\,op\,b)\,op\,c = b\,op\,(a\,op\,c)。

  • 化为规范格式 (canonical format):积之和、项按规范序,如 (a+3)(a+8)⋅4⇒4a2+44a+96(a+3)(a+8)\cdot4 \Rightarrow 4a^2+44a+96(Whale book §12.3.1)。 :::

(a/b)*0 + c 可化简为 c;但当 b=0 时本应抛除零异常,化简后却得到结果——化简可能改变语义,需谨慎(尤其浮点)。


我的理解

优化规则的核心不是「看起来更快」,而是一个可说清的证明义务:在哪些语义假设下,改写前后的可观察行为相同。把除零、溢出和浮点特例写进规则边界,比多收集几个模式更重要。

7. 块内程序分析与优化小结

::: theorem 定理(基本块优化的共性)

  • 分析与变换都符号化模拟程序执行:CSE、复制传播、常量传播前向;死代码消除逆向。

  • 变换层叠协作:一个变换常制造低效代码,由后续变换清理;即便原代码不直接受益,某变换仍可能有用(为后续铺路)。 :::

  • 基本块内:值编号/CSE、复制传播、死代码消除、代数化简、常量传播、强度削弱。

  • 临时变量通常单赋值,便于推理;程序变量可跨块、可多次赋值。

  • 这些思想将在 L8 推广到跨基本块(全过程)的数据流分析。