Archipelago

Lec 24静态分析 II(Static Analysis II)——格、转移函数、不动点与终止

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

当前课程暂无后续已收录内容。

Lec 24 静态分析 II(Static Analysis II)——格、转移函数、不动点与终止

接 L23。本讲把数据流框架补完整:用偏序/格 (lattice) 形式化"事实空间",定义 top/bottom、join/meet;写出转移函数与 CFG 上的方程组;用混沌迭代求解,并证明它终止(单调性 + 完备格 + 有限上升链,Knaster–Tarski)。最后把整个框架套到课程里所有具体分析上(常量/类型/栈缓存/逃逸/到达定义)。

TL;DR

  • 格用偏序描述事实的精度,⊑ 表示无信息,⊒ 表示最不精确的安全事实,控制流汇合通过 join 组合路径。
  • 语句的单调转移函数把 CFG 变成方程组,混沌迭代 / 工作表从初始事实出发反复传播,直到达到最小不动点。
  • 完备格与单调性保证不动点存在,有限升链保证有限步终止;无限域则需有限抽象或 widening 在精度与收敛之间取舍。

1. 五个关键概念(回顾)

事实 (Facts,是个格) → 转移函数 (Transfer Functions) → 合并 (Merging, join/meet) → 算法 (Chaotic Iteration) → 终止 (Fixpoints)。L23 讲了前面,本讲补齐格的形式化、转移函数、迭代算法、终止性。


2. 偏序、Top 与 Bottom

定义(偏序与 Hasse 图)

在事实集合 P 上定义偏序 ⊑("精度"序)。Hasse 图里"从 y 到 x 的箭头/连线"表示 x ⊑ y(x 在下、更精确;y 在上、更粗)。

定义(Top ⊤ 与 Bottom ⊥)

  • ⊤(top):P 的最大元——通常是最不精确的事实,意为"变量可取任意值"。这就是 L23 那个 "?"。

  • ⊥(bottom):P 的最小元——通常表示"尚未处理 / 无信息"。例如值分析里代表空集:还不知道任何可能取值(意味着该变量在分析里尚未执行到,或是不可达代码)。 :::

注意方向:分析从 ⊥(无信息)开始上升,合并使事实变粗(向 ⊤ 靠)。


3. 格(Lattice)

对集合 P 与偏序 ⊑,若任意 x,y∈P 都存在

  • join x ⊔ y(最小上界 lub)与

  • meet x ⊓ y(最大下界 glb),

则 P 是一个格。合并控制流分支用 join(取最小上界)。

定义(覆盖 Covering)

x ⊏ y 指 x ⊑ y 且 x≠y;称 y 覆盖 x,若 x ⊏ y 且不存在中间元 z 使 x ⊏ z ⊏ y。Hasse 图里 y 覆盖 x 就画一条 y 在上、x 在下的连线。

例:布尔格 / 超立方体 P = {000,…,111},x ⊑ y ⟺ (x & y) = x(按位与)。各种分析的格:常量(扁平格)、符号(0/+/−/⊤,可加 nonneg/nonpos 加深)、有界区间、有界值集合,精度依次提高。

3.1 用 join 重做合并

L23 的"合并两支 b=-1 与 b=1"现在有了统一答案——在各自的格里取 join:

分析join(-1, 1)
常量⊤(非常量)
符号⊤(任意符号)
区间[-1, 1]
值集合{-1, 1}

4. 转移函数(Transfer Functions)

定义(转移函数)

刻画单条语句/基本块对事实的影响:给定语句前成立的事实,算出语句后成立的事实。每种分析(格)配一套转移函数。有限格上可用位向量运算廉价实现。关键要求:转移函数必须单调 (monotone)。

常量分析的转移函数(操作数事实是整数常量或 ⊤):

x = n        ⟹  fact(x) = n
x = y + z    ⟹  fact(x) = fact(y) + fact(z)   若两者皆常量,否则 ⊤
x = y - z    ⟹  同理

符号分析的转移函数(事实 ∈ {0,+,−,⊤}):

x = n        ⟹  fact(x) = sign(n)
x = y + z    ⟹  如 (+)+(+)=+,(+)+(−)=⊤(符号未定)……
x = y - z    ⟹  按符号规则

要点:为任意程序性质构造格,并保证转移函数单调——这是终止性证明的前提。


5. 前向数据流分析:程序 → 方程组

定义(前向数据流方程)

把 CFG 每个结点 n 配一对事实,分支汇合处用 join 合并各前驱的输出,结点内用转移函数。前向分析(如常量传播)沿 CFG 正向传播;活跃性那种是后向。

三步走(Checkpoint):

  1. 定义程序性质的格;
  2. 定义每条语句"前置事实 → 后置事实"的转移函数;
  3. 由此得到一组方程,迭代求解。

剩下的问题:这个迭代会终止吗?


6. 混沌迭代(Chaotic Iteration)

定义(混沌迭代 / 工作表)

把待处理结点放入队列;取出一个,重算其事实(join 前驱 + 转移函数);若其结果变化,则把受影响的后继加入队列("已在队列里就不重复加");直到队列空——此时所有方程同时成立,达到不动点。

slides 用 s=0;a=4;i=0; if(k==0)b=1 else b=2; while(i<n){s=s+a*b;i=i+1} return s 把"b 是否常量"逐结点(编号 0–5)迭代了几十步,最终 b 在循环体处收敛到 ⊤(非常量)——因为两条到达定义 b=1 与 b=2 在汇合点 join 成 ⊤。而 a 始终收敛到常量 4。


7. 终止性(Termination / Fixpoints)

为什么不会"跑不完"?考虑 i=0; while(i<n) i=i+1;——若分析追踪 i 的精确值,它会 0,1,2,… 永远上升,runs forever!

定理(终止性的三个充分条件)

设计分析使每步事实单调上升(要么同样精确、要么更不精确),迭代到最小不动点 (least fixpoint)——最精确的那个不动点。要保证可行:

  • 转移函数单调 (monotone):分析总沿格上升;

  • 格完备 (complete):总存在最小上界可达;

  • 有限上升链 (finite ascending chains):到达最小不动点所需迭代步数有限。

单调 + 完备 ⟹ 必有最小不动点;有限上升链 ⟹ 有限步到达。

定义(完备格 Complete Lattice)

若任意子集 S⊆P 都有 ⊔S 与 ⊓S,则 P 是完备格。完备性保证所有链都有上界。所有有限格都是完备的。

例题(两类失败)

  • 不完备的格:整数 Z 配标准 ≤(join=max, meet=min)是格,但没有 ⊤/⊥;区间格 Z 上的 P 也不完备——补上 {−∞, +∞} 后才完备。

  • 无限上升链:精确值分析里 0⊏1⊏2⊏… 永不收敛 → 迭代不终止。解法:用有限高度的抽象(符号、有界区间)或加 widening。

理论基础:Knaster–Tarski 定理——完备格上的单调函数必有(最小)不动点。


8. 框架套用:课程里所有分析都是它的实例

定义(具体分析 = 选定的格 + 转移函数)

  • 常量传播:整数上的扁平格(⊥ / 各整数 / ⊤);

  • 类型分析:类型的扁平格(Int/String/None/Record/Function),每变量一个格值 → 消除类型检查或特化算子(如 + 两边都是 string);可追踪值子集提精度;

  • 栈缓存(寄存器分配):用常量传播的格追踪当前栈深度,每深度预先绑定一个寄存器(深度 1→%r12, 2→%r13, 3→%r14, 4→%r15);初值=栈长 0;转移函数按每条指令 push/pop 数增减抽象栈长;

  • 逃逸分析 (Escape Analysis):格值附在"构造对象的操作"上,判断对象是否逃逸函数(private / may-escape)。逃逸四途径:传给别的调用、被返回、存入 record 字段、存入全局变量。不逃逸的对象可栈分配而非堆分配。

  • 到达定义 + 常量传播:定义到达使用,若所有到达定义赋同一常量,则该使用是常量、可替换。 :::

若每个变量的值是一个格,则整个程序状态是所有变量状态的积格 (product lattice)。于是可把 Types × Constants × StackDepth 等组合成一个大格统一迭代。


9. 实现的三种思路

定义(静态分析实现的三条路)

  • "凭感觉"直接写代码:多半能跑,但为何正确的论证很 ad hoc;

  • 把数据流当语义框架:用格/join/meet/转移函数/单调性/终止来组织并论证实现的正确性(推荐心智模型);

  • 把数据流当实现框架:写一个由"转移函数 + 格"参数化、自动跑工作表算法的通用引擎——生产编译器常这么做(多种分析复用一套算法),但对 P5 可能引入过多复杂度。 :::


10. 展望:静态分析 vs 动态分析

静态分析会给动态编译带来可观开销——要确保收益值回票价。替代方案:动态分析——运行时记录程序轨迹与成立的事实,按这些事实生成优化代码,并插入守卫 (guards):一旦程序行为变化、事实不再成立,就跳回未优化代码。

难点:在哪、生成哪些 guard?——意外的是,这本身又需要静态分析。

这正是现代 JIT(V8、SpiderMonkey)的"推测优化 + 去优化 (deoptimization)"思路,与 L18 的自适应/剖析驱动编译呼应。


11. 与参考资料的对应(补充)

  • 格/不动点框架 credit Saman Amarasinghe (6.035);Knaster–Tarski、抽象解释见 Cousot²(1977)、姊妹课 6.5110《Foundations of Program Analysis》(index.md)。
  • 逃逸分析对应 index.md 的 GC 阅读(栈/堆分配权衡);推测优化见 index.md 的 V8/SpiderMonkey 博客。
  • Crafting Interpreters 不涉及数据流分析;系统学习见《Engineering a Compiler》。

12. 本讲小结(也是 Phase 5 / 全课收尾)

  • 事实空间形式化为格:偏序刻画精度,⊤=最不精确("?")、⊥=无信息;任意两元有 join(lub)/meet(glb),合并分支用 join。
  • 转移函数刻画语句对事实的影响,必须单调;程序变成 CFG 上的方程组,用混沌迭代/工作表求最小不动点。
  • 终止性 = 单调转移函数 + 完备格 + 有限上升链(Knaster–Tarski 保证最小不动点存在;有限链保证有限步到达);精确值分析因无限上升链不终止,需有限抽象或 widening。
  • 课程所有分析都是同一框架的实例:常量/类型/栈缓存/逃逸/到达定义,多变量用积格组合。
  • 实现可"凭感觉 / 当语义框架 / 当实现框架";静态分析开销大时可转向动态分析 + 守卫/去优化(但选 guard 又要静态分析)。
  • 至此 24 讲全部完成:解析 → 语义 → GC → 低级 VM → 代码生成/优化 → 寄存器分配 → 静态分析,构成一条完整的"动态语言虚拟机"实现与优化链路。