前言
上一节中,我们学习了偏序、格、格上的函数单调性和不动点定理等数学知识,留下了关于迭代算法的三个问题没有解答:
- 这个算法能够保证一定能终止(到达不动点/有解)吗?
- 如果上一个问题的答案是肯定的,那么只有一个解/不动点吗?如果有多个不动点,哪一个是最好的、或者说最精确的?
- 什么时候这个算法能够到达不动点呢?
本节,我们将陆续来回答这三个问题,并学习一种新的数据流分析应用——常量传播,以及一种基于迭代算法但做了改进的Worklist算法。另外,本节课将引出第二个实验作业——常量传播与Worklist求解器。
将迭代算法关联到不动点定理
首先,我们要将迭代算法与不动点定理关联起来,这样才能够利用不动点定理来解答上述问题。再来看一下不动点定理:
给定一个完全格$(L, \preccurlyeq)$,如果$f: L \rightarrow L$是单调的且$L$是有限的,那么:
- $f$的最小不动点可以通过以下方式得到:不断迭代计算$f(\bot)), f(f(\bot)),\ …\ , f^{k}(\bot))$,直到到达一个不动点。
- $f$的最大不动点可以通过类似的方式得到:不断迭代计算$f(\top)), f(f(\top)),\ …\ , f^{k}(\top))$,直到到达一个不动点。
在上一节课的开头,我们学习了这样一种观点——may&forward analysis算法可以视作不断输出一系列的k-tuple,直到某个k-tuple与上一个k-tuple相同:
$$ \text{iter 0}: (\bot, \bot,\ …\ , \bot) $$
$$ \text{iter i}: (v_{1}^{i}, v_{2}^{i},\ …\ , v_{k}^{i}) = X_{i} = F(X_{i-1}) $$
$$ \text{iter i+1}:(v_{1}^{i+1}, v_{2}^{i+1},\ …\ , v_{k}^{i+1}) = X_{i+1} = F(X_{i}) = X_{i} $$
上一节中,我们已经知道CFG上每个节点的OUT都对应一个完全(有限)格$L$,整个CFG上所有节点的OUT就构成了一个乘积格。对于乘积格$L^{k}$来说,如果构成它的每个格都是完全(且有限)格,那么$L^{k}$也是完全(且有限)格。至此,迭代算法满足不动点定理的第二个条件(格的有限性)。
接下来,我们需要证明迭代算法对应的函数$F$是单调的。根据之前的知识,$F$由两部分组成:
- transfer function $f$。
- join/meet function $\lfloor\rfloor/\lceil\rceil: L \times\ …\ \times L \rightarrow L$(控制流汇聚)。
根据之前的知识,Gen/Kill总是单调的,因此transfer function $f$是单调的。现在来证明一下join/meet function也是单调的,以join为例:$\forall\ x, y, z \in L, x \preccurlyeq y$,证明$x \lfloor\rfloor z \preccurlyeq y \lfloor\rfloor z$。
- 由$\lfloor\rfloor$的定义(最小上界)可知,$y \preccurlyeq y \lfloor\rfloor z$。
- 根据$\preccurlyeq$的传递性可知,$x \preccurlyeq y \lfloor\rfloor z$。
- 因此,$y \lfloor\rfloor z$是$x$的上界,也是$z$的上界。
- 而$x \lfloor\rfloor z$是$x$和$z$的最小上界,因此$x \lfloor\rfloor z \preccurlyeq y\lfloor\rfloor z$。
于是,我们证明了join function也是单调的,故不动点定理适用于前述迭代算法。
因此,根据不动点定理,前述迭代算法一定能达到不动点(问题1),且一定能够到达最小(最大)不动点(问题2)。
第三个问题实际上是问在最坏情况下,需要经过多少次迭代才能达到不动点。我们假设每次迭代只在一个节点OUT对应的格上走了一步,而格的高度是$h$(在格上从top到bottom的最长路径),CFG有$k$个节点,那么最多需要$i = h*k$次迭代才能达到不动点。至此,三个问题回答完毕。
格视角下的May/Must分析
接下来,李樾老师探讨了如何从格理论的视角来看待may和must分析。
假设下面这个幂集表示CFG中各节点的OUT对应的格,图中最上方是$\top$,最下方是$\bot$:
相应地,下图中左右两个竖立的椭圆分别代表在must分析和may分析中CFG各节点OUT组成的乘积格的结果:
无论是must还是may分析,都是从unsafe result向safe result方向移动,“从准往不准走”。其中must分析在这个乘积格上来看就是不断从$\top$往下走,越过truth(第一节课中讨论过sound、truth和complete的关系),我们的迭代算法会达到最大不动点;相反,may分析就是不断从$\bot$往上走,越过truth,我们的迭代算法会达到最小不动点。
为什么不动点一定在safe区域呢?这和迭代算法中的safe approximation有关,是需要设计来保证的。
这部分内容有点抽象,后面如果要用到相关知识,还是回过头来再听一下李樾老师的课程视频吧。
MOP和分配性(Distributivity)
接下来研究一下分析精度:我们得出的解的精度如何呢?这里介绍一下另一种解法:Meet-Over-All-Paths(MOP)。
我们定义$P$为从Entry到某个语句$S_{i}$的某一条路径(可能还会有别的路径),转换函数$F_{P}$是从Entry到$S_{i}$经过的所有的转换函数($f_{S_{1}},\ …\ , f_{S_{i-1}}$)构成的复合转换函数,那么有:
$$ MOP[S_{i}] = \lfloor\rfloor / \lceil\rceil \ F_{P}(OUT[Entry]) $$
换言之,MOP在每条路径末尾计算数据流的值,然后将它们join/meet起来,寻找它们的lub/glb。
然而,有些路径可能是假的,例如,某些代码分支在动态运行时是永远不会执行到的。因此,MOP可能是不精确的。另外,MOP可能是无法终止的(unbounded),如遇到循环的情况;还可能是不可枚举的(not enumerable)。
那么,迭代算法求出的解和MOP求出的解的关系是什么呢?我们结合下面这个例子来说明:
针对这个例子,我们有:
$$ IN[S_{4}] = f_{S_{3}}(f_{S_{1}}(OUT[Entry])\ \lfloor\rfloor\ f_{S_{2}}(OUT[Entry])) $$
$$ MOP[S_{4}] = f_{S_{3}}(f_{S_{1}}(OUT[Entry]))\ \lfloor\rfloor\ f_{S_{3}}(f_{S_{2}}(OUT[Entry])) $$
抽象一下,我们可以得到:
$$ Ours = F(x \lfloor\rfloor y) $$
$$ MOP = F(x) \lfloor\rfloor F(y) $$
很好理解,迭代算法会在过程中做join/meet操作,而MOP则是在路径最后才做join/meet操作。
在此基础上,我们来讨论一下迭代算法结果和MOP结果的关系:
- 根据$\lfloor\rfloor$(最小上界)的定义可知,$x \preccurlyeq x \lfloor\rfloor y$且$y \preccurlyeq x \lfloor\rfloor y$,
- 转换函数$F$是单调的,因此有$F(x) \preccurlyeq F(x \lfloor\rfloor y)$及$F(y) \preccurlyeq F(x \lfloor\rfloor y)$,
- 故$F(x \lfloor\rfloor y)$是$F(x)$和$F(y)$的上界,而$F(x) \lfloor\rfloor F(y)$又是$F(x)$和$F(y)$的最小上界,
- 因此可得$F(x) \lfloor\rfloor F(y) \preccurlyeq F(x \lfloor\rfloor y)$,即$MOP \preccurlyeq Ours$。
也就是说,我们的迭代算法的“精确度”通常要小于等于MOP。当转换函数$F$具有分配性(distributive)时,$MOP = Ours$,两者精确度才相同。事实上,我们在学习迭代算法时遇到的bit vector也好,gen/kill也好,集合交并集操作也好,都是distributive的。
然而,有一些数据流分析应用是not distributive的,比如下面这个常量传播分析。
常量传播(Constant Propagation)
老规矩,首先来看定义:
Given a variable x at program point p, determine whether x is guaranteed to hold a constant value at p.
根据该定义,我们可以确定的是CFG中每个节点的OUT应该是一个由$(x, v)$对组成的集合,其中$x$是变量,$v$是在该节点后$x$的值。
上一节中,我们定义一个基于格的数据流分析框架$(D, L, F)$:其中$D$指的是数据流的方向,包括前向和后向;$L$指的是由值域$V$和一个meet或join操作符构成的格;$F$指的是从$V$到$V$的一系列transfer functions。
首先是方向$D$。很明显,常量传播分析应该是一个前向分析,因为对于每个节点来说,需要考虑的是前驱节点对它的影响。
接下来讨论格$L$,它的值域$V$如下所示,由UNDEF、常量和NAC(not a constant)组成:
操作符是meet $\lceil \rceil$,它需要根据不同情况执行不同的操作:
- $\text{NAC} \lceil \rceil v = \text{NAC}$
- $\text{UNDEF} \lceil \rceil v = v$
- $c \lceil \rceil v =\ ?$
- $c \lceil \rceil c = c$
- $c_{1} \lceil \rceil c_{2} = \text{NAC}$
上述操作完全是根据实际需求来定义的。例如,我们并不关心未初始化的变量(UNDEF)。
接下来考虑转换函数。给定一个语句$s: x = …$,我们将其转换函数定义为:
$$ F: OUT[s] = gen\ \cup\ (IN[s] - \lbrace (x, \underline{\ \ } ) \rbrace) $$
下面来看如何针对三类不同的语句生成$gen$:
- $s: x = c$,则$gen = \lbrace (x,c) \rbrace$
- $s: x = y$,则$gen = \lbrace (x,val(y)) \rbrace$
- $s: x = y\ op\ z$,则$gen = \lbrace (x,f(y,z)) \rbrace$
其中$f(y,z)$定义如下:
$$ f(y, z) = \begin{cases} val(y)\ op\ val(z) &\text{if val(y) and val(z) are constants} \\ \text{NAC} &\text{if val(y) or val(z) is NAC} \\ \text{UNDEF} &\text{otherwise} \end{cases} $$
另外,如果$s$不是一个赋值语句,那么转换函数$F$不起作用,相当于空函数即可。
下面这个小例子说明,常量传播分析是not distributive的:
Worklist算法
OK,最后来看一下worklist算法。
思想其实很简单,就是维护了一个worklist,根据worklist是否为空判断迭代是否停止,只把OUT变化的节点的后继加入到worklist中,从而不必每次迭代都遍历CFG所有节点。
Worklist算法的伪代码如下图所示:
总结与思考
至此,数据流分析的理论部分结束。本节课,我们学习了如何将迭代算法与不动点定理关联、如何在格的视角下理解may/must分析、MOP和分配性、常量传播分析和Worklist算法。最后两部分其实也可以算作数据流分析应用了。
这两节理论课不分伯仲。个人感觉上节课的内容更复杂,但是这节课的内容更抽象。
话不多说,A2实验见。