【编译原理】中间代码优化(三) 循环优化

循环优化概述.

什么叫做循环?循环就是程序中那些可能反复执行的代码序列。也正是由于这部分代码序列可能会被反复执行,所以在进行中间代码优化时应着重考虑循环优化,这对提高目标代码的效率起到很大的作用。为了进行循环优化,首先需要确定的是程序流图中哪些基本块构成一个循环。按照结构程序设计思想,程序员在编程时应使用高级语言所提供的结构性的循环语句来编写循环。而由高级语言的循环语句所形成的循环,是不难找出的。对于循环中的代码,可以实行代码外提、强度削弱和删除归纳变量等优化操作。一个中间代码序列的程序流图如下所示:
【编译原理】中间代码优化(三) 循环优化_第1张图片
我们不难看出,其中B 2 _2 2和B 3 _3 3分别构成一个循环,{B 2 _2 2,B 3 _3 3,B 4 _4 4,B 5 _5 5}构成一个范围更大的循环。而我们要进行循环优化,首先需要知道循环在哪里,接下来,我们就一步步给出,整个循环优化的过程。

计算必经节点集.

【定义】:若从首结点出发到达结点N j _j j的每一条通路都必须经过结点N i _i i,那么我们称N i _i i是N j _j j的必经节点,记为N i _i i dom N j _j j,其中dom是dominate的简写。那么N j _j j所有必经节点的集合,就是N j _j j的必经节点集,记为D(N j _j j).

计算程序流图中必经节点集的算法也是一个不动点算法,通过每一次的迭代来计算一个结点的必经节点集。当程序流图中所有结点的迭代计算结果都不发生改变时,说明已经获得了最终的结果,算法宣布结束。下面我们给出计算必经节点集的算法:
【编译原理】中间代码优化(三) 循环优化_第2张图片
这里我们也给出一个例题,作为计算必经节点集算法的收尾:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第3张图片
需要注意的是,该算法并不像图中那样,一次迭代计算就可以确定结果,而是需要再一次迭代确认了所有的结点都没有发生变化,才能确定最终结果的。

循环查找算法.

1.查找回边.

【定义】如果一个程序流图中存在有向边 i _i i→N j _j j>,并且N j _j j∈D(N i _i i),那么我们称有向边 i _i i→N j _j j>是一条回边。

依旧是我们前一部分给出的实例,考察其中的回边:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第4张图片

2.查找循环.

求出了程序流图中的回边之后,我们就可以基于回边来查找循环。具体的方法是,若 i _i i→N j _j j>是一条回边,那么该回边构成的循环中的结点包括:N i _i i和N j _j j以及所有不经过N i _i i能够到达N j _j j的结点。我们以上图中求出的三条回边为例,6→6确定的循环中只包括结点6;7→4确定的循环中包括结点有{4,7,5,6};4→2确定的循环中包括的结点有{2,4,3,5,6,7}。

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第5张图片

代码外提.

至此我们已经完成了查找循环包含结点的工作,接下来就是要对循环体中的中间代码进行优化了。循环中的某些代码虽然随着循环反复地执行,但其中某些运算的结果并没有发生改变,例如循环中有形如A:=B op C的代码,并且如果B和C都是常数,或者到达它们的定值点都在循环外,那么不管循环多少次,每次计算出来得到的B op C的结果都是不变的,对于这样的不变运算,我们完全可以将其外提到循环以外,避免其随着循环多次计算。如此一来,程序的结果没有发生变化,但程序的运行速率却得到了一定程度的提高,这就是代码外提。

【定义】所谓变量A在程序中某一点d的定值到达了另一点u,或者说变量A的定值点d达到另一点u,指的是程序流图中从d有一条通路到达u,并且通路上没有A的其它定值点。

实行代码外提时,我们在循环的入口结点之前建立一个新结点(基本块),称为循环的前置结点,该前置结点以循环的入口结点为其唯一后继。并且原来流图中从循环外引到循环入口结点的有向边,都改为引到该前置结点。如下图所示:
【编译原理】中间代码优化(三) 循环优化_第6张图片
我们考虑的循环结构,其入口结点都是唯一的,从而其前置结点也是唯一的,我们后续进行代码外提时所有外提的代码都将提到前置结点中。下面一段Pascal源程序,是我们叙述代码外提的第一个例子:

for I:=1 to 10 do
	A[I,2*J]:=A[I,2*J]+1

它对应的中间代码序列如下:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第7张图片

  1. 考察序号为3和7的语句,因为循环中并没有J的定值点,所以其中J所有引用的定值点都在循环外,从而3和7都是循环不变运算;
  2. 考察序号为6和10的语句,分配给数组A的首地址addr(A)的值并不会随着循环的一次次执行而改变,所以6和10也都是循环不变运算。

所以我们可以做出这样的判断:3与7、6与10都可以外提到该循环的前置节点中,进行了代码外提之后的中间代码序列如下所示:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第8张图片
那么问题又来了,是不是在任何情况下,都可以将循环不变运算进行外提呢?我们看第二个例子:
【编译原理】中间代码优化(三) 循环优化_第9张图片
从程序流图中我们不难看出,{B 2 _2 2,B 3 _3 3,B 4 _4 4}构成了一个循环,并且B 2 _2 2是入口结点,B 4 _4 4是出口结点。

【定义】出口结点是指循环中具有如下性质的结点:从该结点有一条有向边引到循环外的某结点。

图中我们也不难看出,基本块B 3 _3 3中的I:=2语句是循环不变语句,那么我们是否可以直接将这一语句外提到循环以外的前置结点B 2 ′ _2' 2呢?我们暂且认为可以这样做,外提之后的程序流图如下所示:
【编译原理】中间代码优化(三) 循环优化_第10张图片
我们看外提了I:=2这一语句的程序流图,执行到基本块B 5 _5 5时,变量I的值总会是2,从而J的值也会是2.但我们需要注意的是,在原始的、没有进行I:=2外提的程序流图中,B 3 _3 3并不是B 4 _4 4的必经结点。我们在原始流图中考虑X=30、Y=25的情况,这样的情况下B 3 _3 3是不会被执行的,所以执行到B 5 _5 5时变量I的值应该是1,从而J的值也应该是1而非2.那么很显然,我们进行了I:=2外提的程序已经改变了程序运行的结果,这必然是不允许的。问题到底出在什么地方呢?其实前面已经点出,B 3 _3 3并不是循环出口结点B 4 _4 4的必经节点,再直白一点说,B 3 _3 3中的代码并不一定会对B 4 _4 4中的代码起到作用,但如果我们将其外提到循环的前置结点,那么该外提的语句必然是会对B 4 _4 4产生影响的,这就导致了程序运行结果被改变的风险,我们并不允许这样的风险存在。
所以从这个例子中我们可以看到,当一个循环不变运算要外提到循环的前置结点时,要求该循环不变运算的语句所在的结点是循环的所有出口结点的必经结点
另外,我们注意到,如果循环中变量I的所有引用点都是B 3 _3 3I的定值点所能到达的,I在循环中不再有其它的定值点,并且出循环之后也不会再引用变量I的值(即在循环外的循环后继结点入口,变量I不是活跃的),那么即使B 3 _3 3不是B 4 _4 4的必经结点,还是可以将I:=2外提到循环的前置结点,因为这样做并不会改变程序的结果。
再一个例子,如果我们将上述程序流图中的基本块B 2 _2 2改为:

I:=3
if X<Y goto B3

考虑B 2 _2 2I:=3外提的问题。
通过计算必经结点集可以发现,B 2 _2 2确实是循环出口结点B 4 _4 4的必经结点,那么我们是否可以将I:=3进行代码外提呢?同样的论述过程,我们姑且认为可以,那么如果程序的执行过程是2-3-4-2-4-5,则执行B 5 _5 5时的变量I值为2,从而J的值也是2;而如果不进行外提操作,那么同样的执行过程下,I和J的值都应该是3,而非2.这一错误的原因在于,循环中除了B 2 _2 2以外,B 3 _3 3也对同一个变量进行了定值。
所以从这个例子中我们可以看到,当我们将循环不变运算A:=B op C外提时,要求循环中的其它地方不再有A的定值点
第三个例子,也是最后一个例子,我们看下面一个程序流图:
【编译原理】中间代码优化(三) 循环优化_第11张图片
考察基本块B 4 _4 4中循环不变运算I:=2这一语句的外提操作。首先,B 4 _4 4结点就是整个循环的出口结点,并且B 4 _4 4结点也是其本身的必经结点;其次整个循环{B 2 _2 2,B 3 _3 3,B 4 _4 4}中也没有第二个对于变量I定值的语句。那么我们是否可以将这一语句外提呢?同样的方法,我们还是暂且认为这一语句可以外提,再比较对于同一个执行过程,外提前后的执行结果。我们考虑X=0、Y=2的情况,循环的执行流程是2-3-4-2-4-5,代码外提前,程序的执行结果为J=2;而代码外提后,程序的执行结果为J=3.
通过这个例子,我们看到,当将循环不变语句A:=B op C进行代码外提时,要求循环中所有对于A的引用点都是并且仅仅是这一定值语句所能到达的。上述的例子中,能够到达A:=I+1这一对于I的引用的定值语句,不仅有B 4 _4 4中的I:=2,还有B 1 _1 1中的I:=1.
最后我们给出查找循环L中不变运算的算法:
【编译原理】中间代码优化(三) 循环优化_第12张图片
以及代码外提算法:
【编译原理】中间代码优化(三) 循环优化_第13张图片

强度削弱.

我们要介绍的第二种循环优化技术叫做强度削弱。强度削弱是将程序中执行时间较长的运算替换为执行时间较短的运算。例如,最常见的就是将循环中的乘法运算用递归的加法运算来代替。我们考察前面经过了代码外提的示例流图:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第14张图片
不难看出{B 2 _2 2,B 5 _5 5}是一个循环,并且B 2 _2 2是循环的入口结点。我们注意序号为13的语句,这里的变量I是一个递归赋值的变量,每循环一次,它增加一个常量1。另外,序号为4和8的语句在计算T 2 _2 2和T 6 _6 6时,都会使用I的值,并且T 2 _2 2和T 6 _6 6都是I的线性函数,每循环一次,它们都增加一个常量10.因此,如果把4和8外提到循环的前置结点中,并且在13号语句之后添加上为T 2 _2 2和T 6 _6 6增加常量10的语句,程序的运行结果依然不变。

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第15张图片
经过上述变换,循环中原来的乘法运算4和8被替换为了在前置结点中进行一次初始化的乘法运算(计算初值)以及在循环中递归赋值的加法运算(4’)和(8’).不仅加法运算一般来说比乘法运算快,而且这种在循环前计算初值,再于循环末尾进行常量递增的运算,可以利用变址器提高运算速度,从而使运算的强度得到削弱。所以我们称这种优化技术为强度削弱。
强度削弱不仅对于乘法可以实行,对于加法也可以实行。例如上图中序号为5和9的语句,T 3 _3 3与T 7 _7 7的计算中引用到了T 2 _2 2和T 6 _6 6,并且T 2 _2 2和T 6 _6 6也是递归赋值的变量,每循环一次,它们的值就增加一个常量10.而T 3 _3 3与T 7 _7 7的另一个运算对象都是循环不变量,所以每循环一次,T 3 _3 3与T 7 _7 7的值就增加一个常量10.很自然地,我们会想到对它们进行强度削弱,即外提一个初始化语句,在循环的末尾添加常量递增语句,得到的优化结果如下所示:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第16张图片
从上面的例子中我们可以看到:

  1. 如果循环中有A的递归赋值A:=A±b 1 _1 1,并且循环中另一个变量B的赋值运算可以写为B:=k*A±b 2 _2 2的形式,那么对于B的赋值运算,我们可以进行强度削弱;
  2. 进行强度削弱之后,循环中可能会出现一些新的无用赋值,例如上图中的(4’)和(8’),因为循环中不再使用T 2 _2 2和T 6 _6 6,那么如果循环出口之后它们也不是活跃变量,是完全可以删除的;
  3. 循环中下标变量的地址计算是相当耗费时间的,这里介绍的方法对削弱下标变量地址计算的强度是非常有效的。前面的例子中,数组是二维的,如果我们考察一个更高维度的数组,将会进一步看到强度削弱的作用。对于下标变量地址计算来说,强度削弱实际上就是实现下标变量地址的递归计算。

删除归纳变量.

我们要介绍的最后一种循环赋值技术叫做删除归纳变量。讲述具体的流程之前,首先给出基本归纳变量以及归纳变量的定义:

【定义】基本归纳变量:循环中对于变量A的赋值只有唯一的、形如A:=A±b 1 _1 1的赋值,那么称A为循环中的基本归纳变量;
【定义】归纳变量:如果A是一个基本归纳变量,循环中对于B的赋值总是可以化归为A的同一个线性函数,即B:=k*A±b 2 _2 2,那么我们称B是一个归纳变量。

考察进行了代码外提之后的示例程序流图:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第17张图片
不难发现变量I是循环{B 2 _2 2,B 3 _3 3}的基本归纳变量,T 2 _2 2和T 6 _6 6是循环中与I同族的归纳变量,再继续发掘,T 3 _3 3和T 7 _7 7也是与I同族的归纳变量。
一个基本归纳变量除了用于自身的递归赋值以外,往往只在循环中用于计算其他的归纳变量和控制循环的进行。经过了强度削弱之后的示例程序流图如下所示:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第18张图片
关注基本块B 3 _3 3可以发现变量I除了用于自身的递归赋值以外,只用于控制循环的进行。这时,我们可以使用与I同族的某一个归纳变量来代替I控制循环的进行,从而删除变量I.例如,我们选取T 3 _3 3(T 7 _7 7与T 3 _3 3都在循环中被引用,所以是一样的,而T 2 _2 2和T 6 _6 6在循环中没有被引用)来代替I, 由于T 3 _3 3可以写为10*I+T1,并且循环的控制条件为I>10,所以用T 3 _3 3代替了I之后,控制条件变为了T3>100+T1.为了不在每一次进行判断时都计算100+T 1 _1 1的值,我们引入新的临时变量R:=100+T 1 _1 1,所以序号为2的if语句改写为:

R:=100+T1
if T3>R goto (15)

然后我们就可以删去变量I了,这一优化技术称为删除归纳变量,也叫变换循环控制条件。如果我们假定T 2 _2 2和T 6 _6 6在循环出口之后也不是活跃的,那么我们完全可以删去这两个临时变量(因为它们在循环中也没有被使用)。但如果我们在选择代替I的变量时选择了T 2 _2 2或T 6 _6 6,那么(4’)或(8’)就不能删除了,最后完成删除归纳变量的中间代码如下:

【下图引用自中南大学徐德智老师的编译原理2020年授课PPT】

【编译原理】中间代码优化(三) 循环优化_第19张图片
删除归纳变量是在强度削弱之后进行的。我们在下面统一给出强度削弱和删除归纳变量的算法:
【编译原理】中间代码优化(三) 循环优化_第20张图片

你可能感兴趣的:(编译原理,程序人生,经验分享)