gitbook/编译原理之美/docs/156878.md
2022-09-03 22:05:03 +08:00

15 KiB
Raw Permalink Blame History

28 | 数据流分析:你写的程序,它更懂

上一讲,我提到了删除公共子表达式、拷贝传播等本地优化能做的工作,其实,这几个工作也可以在全局优化中进行。

只不过全局优化中的算法不会像在本地优化中一样只针对一个基本块。而是更复杂一些因为要覆盖多个基本块。这些基本块构成了一个CFG代码在运行时有多种可能的执行路径这会造成多路径下值的计算问题比如活跃变量集合的计算。

当然了,还有些优化只能在全局优化中做,在本地优化中做不了,比如:

  • 代码移动code motion能够将代码从一个基本块挪到另一个基本块比如从循环内部挪到循环外部来减少不必要的计算。
  • 部分冗余删除Partial Redundancy Elimination它能把一个基本块都删掉。

总之全局优化比本地优化能做的工作更多分析算法也更复杂因为CFG中可能存在多条执行路径。不过我们可以在上一节课提到的本地优化的算法思路上解决掉多路径情况下V值的计算问题。而这种基于CFG做优化分析的方法框架就叫做数据流分析。

本节课我会把全局优化的算法思路讲解清楚借此引入数据流分析的完整框架。而且在解决多路径情况下V值的计算问题时我还会带你学习一个数学工具半格理论。这样你会对基于数据流分析的代码优化思路建立清晰的认识从而有能力根据需要编写自己的优化算法。

数据流分析的场景:活跃性分析

上一讲,我已经讲了本地优化时的活跃性分析,那时,情况比较简单,你不需要考虑多路径问题。**而在做全局优化时,情况就要复杂一些:**代码不是在一个基本块里简单地顺序执行而可能经过控制流图CFG中的多条路径。我们来看一个例子例子由if语句形成了两条分支语句

基于这个CFG我们可以做全局的活跃性分析从最底下的基本块开始倒着向前计算活跃变量的集合也就是从基本块5倒着向基本块1计算

**这里需要注意,**对基本块1进行计算的时候它的输入是基本块2的输出也就是{a, b, c}和基本块3的输出也就是{a, c},计算结果是这两个集合的并集{a, b, c}。也就是说基本块1的后序基本块有可能用到这三个变量。这里就是与本地优化不同的地方我们要基于多条路径来计算。

基于这个分析图我们马上发现y变量可以被删掉因为它前面的活变量集合{x}不包括y也就是不被后面的代码所使用并且影响到了活跃变量的集合。

删掉y变量以后再继续优化一轮会发现d也可以删掉。

d删掉以后2号基本块里面已经没有代码了也可以被删掉最后的CFG是下面这样

到目前为止我们发现全局优化总体来说跟本地优化很相似唯一的不同就是要基于多个分支计算集合的内容也就是V值。在进入基本块1时2和3两个分支相遇meet我们取了2和3V值的并集。这就是数据流分析的基本特征,你可以记住这个例子,建立直观印象。

但是上面这个CFG还是比较简单的因为它没有循环属于有向无环图。**这种图的特点是:**针对图中的每一个节点,我们总能找到它的前序节点和后序节点,所以我们只需要按照顺序计算就好了。但是如果加上了环路,就不那么简单了,来看一看下面这张图:

基本块4有两个后序节点分别是5和1所以要计算4的活跃变量就需要知道5和1的输出是什么。5的输出好说但1的呢还没计算出来呢。因为要计算1就要依赖2和3从而间接地又依赖了4。**这样一来1和4是循环依赖的。**再进一步探究的话你发现其实1、2、3、4四个节点之间都是循环依赖的。

所以说一旦在CFG中引入循环回路严格的前后计算顺序就不存在了。那你要怎么办呢?

其实我们不是第一次面对这个处境了。在前端部分我们计算First和Follow集合的时候就会遇到循环依赖的情况只不过那时候没有像这样展开细细地分析。不过你可以回顾一下17讲18讲,那个时候你是用什么算法来破解僵局的呢?是不动点法。**在这里,我们还是要运用不动点法,具体操作是:**给每个基本块的V值都分配初始值也就是空集合。

然后对所有节点进行多次计算直到所有集合都稳定为止。第一遍的时候我们按照5-4-3-2-1的顺序计算实际上采取任何顺序都可以计算结果如下

如果现在计算就结束我们实际上可以把基本块2中的d变量删掉。但如果我们再按照5-4-3-2-1的顺序计算一遍就会往集合里增加一些新的元素在图中标的是橙色。**这是因为,**在计算基本块4的时候基本块1的输出{b, c, d}也会变成4的输入。这时我们发现进入基本块2时活变量集合里是含有d的所以d是不能删除的。

你再仔细看看这个d是哪里需要的呢**是基本块3需要的**它会跟1去要1会跟4要4跟2要。所以再次证明1、2、3、4四个节点是互相依赖的。

我们再来看一下,对于活变量集合的计算,当两个分支相遇的情况下,最终的结果我们取了两个分支的并集。

在上一讲我们说一个本地优化分析包含四个元素方向D、值V、转换函数F和初始值I。在做全局优化的时候我们需要再多加一个元素就是两个分支相遇的时候要做一个运算计算他们相交的值这个运算我们可以用大写的希腊字母Λlambda表示。包含了D、V、F、I和Λ的分析框架就叫做数据流分析。

那么Λ怎么计算呢研究者们用了一个数学工具叫做“半格”Semilattice帮助做Λ运算。

直观地理解半格理论

如果要从数学理论角度完全把“半格”这个概念说清楚需要依次介绍清楚“格”Lattice、“半格”Semilattice和“偏序集”Partially Ordered Set等概念。我想这个可以作为爱好数学的同学的一个研究题目或者去向离散数学的老师求教。在我们的课程里,我只是通过举例子,让你对它有直观的认识。

首先,半格是一种偏序集。偏序集就是集合中只有部分成员能够互相比较大小。**举例来说会比较直观。**在做全局活跃性分析的时候,{a, b, c}和{a, c}相遇,产生的新值是{a, b, c}。我们形式化地写成{a, b, c} Λ {a, c} = {a, b, c}。

这时候我们说{a, b, c}是可以跟{a, c}比较大小的。那么哪个大哪个小呢?

如果XΛY=X我们说X<=Y。

所以,{a, b, c}是比较小的,{a, c}是比较大的。

当然,{a, b, c}也可以跟{a, b}比较大小,但它没有办法跟{c, d}比较大小。所以把包含了{{a, b, c}、{a, c}、{a, b}、{c, d}…}这样的一个集合,叫做偏序集,它们中只有部分成员之间可以比较大小。哪些成员可以比较呢?就是下面的半格图中,可以通过有方向的线连起来的。

半格可以画成图形理解起来更直观假设我们的程序只有a, b, c三个变量那么这个半格画成图形是这样的

沿着上面图中的线,两个值是可以比较大小的,按箭头的方向依次减少:{}>{a}>{a, b}> {a, b, c}。如果两个值之间没有一条路径,那么它们之间就是不能比较大小的,就像{a}和{b}就不能比较大小。

对于这个半格,我们把{}空集叫做TopTop大于所有的其他的值。而{a, b, c}叫做Bottom它是最小的值。

在做活跃性分析时我们的Λ运算是计算两个值的最大下界Greatest Lower Bound。怎么讲呢就是比两个原始值都小的值中取最大的那个。{a}和{b}的最大下界是{a, b}{a, b, c} 和{a, c}的最大下界就是{a, b, c} 。

  • 如果一个偏序集中,任意两个元素都有最大下界,那么这个偏序集就叫做交半格Meet Semilattice

  • 与此相对应的,如果集合中的每个元素都有最小上界Least Upper Bound那么这个偏序集叫做并半格Join Semilattice

  • 如果一个偏序集既是交半格,又是并半格,我们说这个偏序集是一个格,示例的这个偏序集就是一个格。

你可能会奇怪,为什么要引入这么复杂的一套数学工具呢?不就是集合运算吗?两个分支相遇,就计算它们的并集,不就可以了吗?**事情没那么简单。**因为并不是所有的分析其V值都是一个集合就算是集合相交时的运算也不一定是求并集而有可能是求交集。

我们通过另一个案例来分析一下非集合的半格运算:常数传播。

数据流分析的场景:常数传播

常数传播就是如果知道某个变量的值是个常数那么就把用到这个变量的表达式都用常数去替换。看看下面的例子在基本块4中a的值能否用一个常数替代

**答案是不能。**到达基本块4的两条路径一条a=3另一条a=4。我们不知道在实际运行的时候会从哪条路径过来所以这个时候a的取值是不确定的基本块4中的a无法用常数替换。

那么,运用数据流分析的框架怎么来做常数传播分析呢?

在这种情况下V不再是一个集合而是a可能取的常数值但a有可能不是一个常数啊所以我们再定义一个特殊的值TopT

除了T之外我们再引入一个与T对应的特殊值Bottom它的含义是某个语句永远不会被执行。总结起来常数传播时V的取值可能是3个

  • 常数c
  • Top意思是a的值不是一个常数
  • Bottom某个语句不会被执行。

**这些值是怎么排序的呢?**最大的是Top中间各个常数之间是无法比较的Bottom是最小的。

接下来我们看看如何计算多个V值相交的值。

我们再把计算过程形式化一下。在这个分析中当我们经过每个语句的时候V值都可能发生变化我们用下面两个函数来代表不同地方的V值

  • C(a, s, in)。表示在语句s之前a的取值比如C(a, b:=a+2, in) = 3。
  • C(a, s, out)。表示在语句s之后a的取值比如C(a, a:=4, out) = 4。

如果s的前序有i条可能的路径那么多个输出和一个输入“C(a, si, out)和C(a, s, in)”的关系,可以制定一系列规则:

1.如果有一条输入路径是Top或者说C(a, si, out)是Top那么结果C(a, s, in)就是Top。

2.如果输入中有两个不同的常数比如3和4那么结果也是Top我们的示例就是这种情况

3.如果所有的输入都是相同的常数或Bottom那么结果就是该常数。如果所有路径a的值都是3那么这里就可以安全地认为a的值是3。那些Bottom路径不影响因为整条路径不会执行。

4.如果所有的输入都是Bottom那么结果也是Bottom。

上面的这4个规则就是一套半格的计算规则。

在这里我们也可以总结一下它的转换规则也就是F考虑一下某个Statement的in值和out值的关系也就是经过该Statement以后V值会有啥变化

1.如果输入是Bottom那么输出也是Bottom。也就是这条路径不会经过。
2.如果该Statement就是“ a := 常数”,那么输出就是该常数。
3.如果该Statement是a赋予的一个比较复杂的表达式而不是常数那么输出就是Top。
4.如果该Statement不是对a赋值的那么V值保持不变。

好了转换函数F也搞清楚了。初始值I是什么呢是Top因为一开始的时候a还没有赋值所以不会是常数方向D是什么呢D是向下。这个时候D、V、F、I和Λ5个元素都清楚了我们就可以写算法实现了。

课程小结

本节课,我们基于全局优化分析的任务,介绍了数据流分析这个框架,并且介绍了半格这个数学工具。我希望你在本讲记住几个要点:

  • 全局分析比本地分析多处理的部分就是CFG因为有了多条执行分支所以要计算分支相遇时的值当CFG存在环路的时候要用不动点法来计算出所有的V值。

  • 数据流分析框架包含方向D、值V、转换函数F、初始值I和交运算Λ5个元素只要分析清楚这5个元素就可以按照固定的套路来编写分析程序。

  • 对于半格理论,关键是要知道如何比较偏序集中元素的大小,理解了这个核心概念,那么求最大下界、最小上界这些也就没有问题了。

**数据流分析也是一个容易让学习者撞墙的知识点,**特别是再加上“半格”这样的数学术语的时候。不过,我们通过全局活跃性分析和全局常数传播的示例,对“半格”的抽象数学概念建立了直觉的理解。遇到全局分析的任务,你也应该能够比照这两个示例,设计出完整的数据流分析的算法了。**不过我建议你,**还是要按照上一讲中对LLVM优化功能的介绍多做几个例子实验一下。

一课一思

如果我们想做一个全局分析用于删除公共子表达式它的数据流分析框架应该是怎样的也就是D、V、F、I和Λ各自应该如何设计呢欢迎分享你的想法。

最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。