什么是数据流分析
编译器的一个重要功能是分析和优化代码。编译时分析(或称静态分析)得到若干信息后,编译器可以确定在何处应用何种变换是安全并且有利可图的。而其中一种重要的分析技术就是数据流分析。顾名思义,数据流分析就是分析数据如何在程序执行路径上流动的技术,那么数据流分析的前提条件就是基于 IR (源代码经过编译得到的中间表示形式)构造 CFG 控制流图。
基于数据流分析,可以实现多种全局优化:
- 复制传播:形如 u=v 的赋值之后,变量 u 用 v 代替
- 常量折叠:若每次运行时表达式的值是常量,则用此常量代替
- 全局公共子表达式:例如表达式 a+b 多处出现而且值都相同,那么可以只计算一次
- 死代码消除:删除其计算结果不被使用的语句
等等。
数据流问题研究的是程序的某个点处的数据流值。
数据流分析的通用方法是在控制流图上定义一组方程并迭代求解,一般分为正向传播和逆向传播。正向传播就是沿着控制流路径,状态向前传递,前驱块的值传到后继块;逆向传播就是逆着控制流路径,后继块的值反向传给前驱块。这里有两个术语:传递函数与控制流约束。传递函数是指基本块的入口与出口的数据流值为两个集合,满足函数关系 f, 正向传播时入口值集 X,则出口值集为 f(X),逆向传播时出口值集 X,则入口值集为 f(X). 控制流约束是在一条路径两端的前驱与后继块的数据流值的传递关系。
数据流分析举例
下面举两个实际例子。
到达定值问题
考虑这样的一个问题,变量 x 在哪些地方被定值,在某个位置使用的 x 是这个值吗?
某个地方变量 x 被赋值了,如果存在路径到达一个点,这个位置 x 被使用了,那么我们说定值 x 到达了此程序点。如果这条路径上 x 被重新定值,我们说 x 被杀死 (kill) 了。可以知道如果某个变量 x 的一个定值 d 到达点 p,那么 p 处使用的 x 的值就可能是 d 定义的。在流图的入口为 x 引入一个未定义值 ⊥,如果 ⊥ 能达到某个 x 的使用,那么说明这个地方的使用可能是未定义值,这就是一个程序错误隐患。
假设有一个程序的控制流图如下所示:
图1 待分析的控制流图
到达定值问题的传递函数被定义为:Out[s] = In[s] + gen - kill。gen 集合是块内的赋值语句产生的新定值,kill 集合是块内赋值语句 kill 的其他定值。对每个变量,有赋值语句则加入到 gen,其他位置的赋值语句都加入 kill。所有块的 gen/kill 集可以一趟扫描完成。路径上的约束为:In[B] = ∪ Out[P],其中P是B的所有前驱块。另外还有边界条件:Out[Entry] = Φ。
建立完方程组之后,循环迭代,每轮迭代中,每个块的 In/Out 集合都在更新。直到所有的 In[s] 与 Out[s] 都不发生变化,此时就是最终的结果。这个结果是保守的,但不是精确的。因为路径是一个不可判定问题,我们只能尽可能保守的包含全部可能路径。因此,某些实际运行中不会走到的路径,也被我们允许穿越定值。伪代码如下:
```
Init:
Out[Entry] = Φ
for each block: Out[B] = Φ
loops:
In[B] = ∪ Out[P]
Out[B] = In[B] + gen - kill
```
用表格表示计算结果如下: