编译器设计中的机器无关代码优化
机器无关代码优化试图通过转换一段不涉及硬件组件(如 CPU 寄存器或任何绝对内存位置)的代码来提高中间代码的效率。一般来说,它通过消除冗余、减少代码行数、消除无用代码或减少重复代码的频率来优化代码。因此,无论机器规格如何,都可以在任何处理器上使用。
可以使用以下方法实现与机器无关的代码优化:
函数保持优化:
函数保留优化处理给定函数中的代码,以尝试减少计算时间。可以通过以下方法实现:
- 公共子表达式消除
- 折叠式的
- 死码消除
- 复制传播
1.公共子表达式消除:
公共子表达式是计算出来的,在最后一次计算后不会改变,但在程序中经常重复。即使它没有改变,编译器也会评估它的值。这样的评估导致资源和时间的浪费。因此最好将其消除。考虑一个例子:
//Code snippet in three address code format
t1=x*z;
t2=a+b;
t3=p%t2;
t4=x*z; //t4 and t1 is same expression
//but evaluated twice by compiler.
t5=a-z;
// after Optimization
t1=x*z;
t2=a+b;
t3=p%t2;
t5=a-z;
如果一个公共子表达式在程序中经常重复出现,那就麻烦了。因此,它需要被淘汰。
2. 恒定折叠:
常量折叠是一种在编译时计算的表达式被其值替换的技术。通常,此类表达式在运行时进行评估,但如果我们将它们替换为它们的值,则无需在运行时进行评估,从而节省时间。
//Code segment
int x= 5+7+c;
//Folding applied
int x=12+c;
折叠可以应用于布尔、整数以及浮点数,但应该小心浮点数。恒定折叠通常与恒定传播交错。
不断传播:
如果任何变量被分配了一个常数值并用于进一步的计算,常数传播建议直接使用该常数值进行进一步的计算。考虑下面的例子
// Code segment
int a = 5;
int c = b * 2;
int z = a;
//Applying constant propagation once
int c = 5 * 2;
int z = a;
//Applying constant propagation second time
int c = 10;
int z = a;
//Applying constant propagation last time
int z = a[10];
3. 死码消除:
死代码是在程序中从未执行或从未到达的程序片段。它是可以有效地从程序中删除而不影响程序的任何其他部分的代码。万一获得了一个值并且以后不再使用,它也被视为死代码。考虑下面的死代码:
//Code
int x= a+23; //the variable x is never used
//in the program. Thus it is a dead code.
z=a+y;
printf("%d,%d".z,y);
//After Optimization
z=a+y;
printf("%d,%d".z,y);
死代码的另一个例子是为变量赋值并在使用它之前更改该值。前面的赋值语句是死代码。这样的死代码需要删除才能实现优化。
4.复制传播:
在使用 x=y 形式的赋值的情况下,复制传播建议使用一个变量而不是其他变量。这些分配是复制语句。我们可以在所有需要的地方有效地使用 y,而不是将其分配给 x。简而言之,消除代码中的副本就是复制传播。
//Code segment
----;
a=b;
z=a+x;
x=z-b;
----;
//After Optimization
----;
z=b+x;
x=z-b;
----;
另一种优化,循环优化处理减少程序在循环中花费的时间。
循环优化:
程序大部分时间都在循环中。因此,循环决定了程序的时间复杂度。因此,为了获得最优且高效的代码,需要循环优化。为了应用循环优化,我们首先需要借助程序流程图使用控制流分析来检测循环。程序流程图中的循环将指示循环的存在。请注意,来自中间代码生成阶段的三地址格式的代码作为优化阶段的输入。这种格式的循环很难识别,因此需要程序流程图。
程序流程图由基本块组成,只不过是将代码分成部分或块并显示代码的执行流程,
上图中的循环表明存在从块 2 到块 3 的循环。
一旦检测到循环,就可以应用以下循环优化技术:
- 频率降低
- 代数表达式简化
- 强度降低
- 冗余消除
1. 降频:
它适用于循环运行尽可能少的代码行的概念。可以通过以下方法实现:
一种。代码运动:
很多时候,在循环中,每次迭代都保持不变的语句包含在循环中。这样的语句是循环不变量,只会导致程序在循环中花费更多时间。代码运动只是将循环不变的代码移到循环外,从而减少在循环内花费的时间。要理解这一点,请考虑以下示例。
//Before code motion
p=100
for(i=0;i
在该示例中,在优化之前,循环不变的代码会针对循环的每次迭代进行评估。一旦应用了代码运动,评估循环不变代码的频率也会降低。因此也称为降频。以下也是代码运动的示例。
//Before code motion
----;
while((x+y)>n)
{
----;
}
----;
// After code motion
----;
int t=x+y;
while(t>n)
{
----;
}
----;
湾。循环展开:
如果一个循环对每次迭代都执行相同的操作,我们可以在循环内多次执行相同的操作。这称为循环展开。这种展开的循环将在单个循环迭代中多次执行评估。
//Before Loop unrolling
while(i<50) //while loop initialize all array elements to 0;
//one element each iteration. Thus the loop runs 50 times.
{
x[i]=0;
i++;
}
//After loop unrolling
while(i<50) //After unrolling, each iteration
//initializes 5 elements to 0;
//Thus this loop runs only 5 times.
{
x[i]=0;
i++;
x[i]=0;
i++;
x[i]=0;
i++;
x[i]=0;
i++;
x[i]=0;
i++;
}
如上例所示,展开的循环比前一个循环更有效。
C。环路干扰:
组合执行相同操作的循环称为循环干扰。
//Before loop jamming
----;
for(i=0;i<5;i++) //Setting all elements of 2D array to 0.
{
for(j=0;j<5;j++)
{
x[i][j]=0;
}
}
for(i=0;i<5;i++) //Setting diagonal elements to 1.
{
x[i][i]=0;
}
----;
//After loop jamming
----;
for(i=0;i<5;i++) //Setting all elements of 2D array to 0
//and diagonal elements to 1.
{
for(j=0;j<5;j++)
{
x[i][j]=0;
x[i][i]=1;
}
}
----;
因此,可以通过只执行一次循环来完成该操作,而不是执行两次相同的循环。
2. 代数表达式化简:
一个程序可能包含一些琐碎的代数表达式,它们不会导致任何有用的计算或值的变化。可以消除这样的代码行,这样编译器就不会浪费时间评估它。例如,
A=A+0;
x=x*1;
这些语句不会导致任何有用的计算。这样的代码可能看起来无害,但是当在任何循环中使用时,它们会继续被编译器评估。因此最好消除它们。
3.强度降低:
它建议用更便宜的操作代替像乘法这样昂贵的操作。
Example:
a*4
after reduction
a<<2
对于在循环中发生数组访问并且只能与整数操作数一起使用的程序,这是一项重要的优化。
4.冗余消除:
可能会发生特定表达式在代码中重复多次的情况。这个表达式对于代码来说是多余的,因为我们可以评估它一次并用它的评估值替换它的下一次出现。这种替代只不过是消除冗余。下面给出一个简单的例子
//Code:
----;
int x=a+b;
----;
int z=a+b+40;
----;
return (a+b)*5;
//After optimization
----;
int x=a+b;
----;
int z=x+40;
----;
return x*5
冗余消除避免了多次评估相同的表达式,从而加快执行速度。