huolong blog

编译优化

优化器是连接编译器前端和后端的重要部分,本文简要介绍一些常见的编译优化手段,需要注意的是编译优化本质上是对程序代码的一种转换,在大多数情况下是对程序的优化,但在某些特殊情况下并不能起到优化效果,具体应用这些优化需要考虑后端架构、程序体积等。

常量折叠和常量传播

编译器优化中有一个指导策略,即尽量把计算时机提前,能够在编译时期计算完成的不要拖到程序执行时,常量折叠和常量传播就是对这种策略的践行。

常量折叠会将只包含常量的表达式或子表达式替换为计算结果,比如:

double a, b;
a = b + 1.0 / 3.0;

替换为: double a, b; a = b + 0.33333;

常量传播会将值已经确定值的变量替换为具体值,例如:

int x = 14;
int y = 7 - x / 2;

转换为:

int x = 14;
int y = 7 - 14 / 2;

常量折叠和常量传播经常会联合使用,常量折叠后可能会产生使用常量传播的机会,同样常量传播后可能会产生使用常量折叠的机会。

函数内联

函数内联会将函数调用替换为具体的函数体代码,例如:

float square(float a) 
{ return a * a;}
float x = square(1.2);

替换为:

float x = 1.2 * 1.2;

函数内联多应用于函数调用次数少,函数体长度短的情况中。其优点如下:

  1. 消除了调用、返回和参数传递的开销。
  2. 如果只有一个内联函数调用,代码就会变小。
  3. 函数内联破坏了函数间调用关系,一定程度上起到了代码混淆作用。

函数内联的缺点是如果要内联的函数被多次调用,且函数体很大,那么代码体积会变大很多。

公共子表达式消除

如果同一个子表达式出现多次,那么编译器可以通过优化只计算一次,例如:

int a, b, c;
b = (a+1) * (a+1);
c = (a+1) / 4;

编译器会替换成这样:

int a, b, c, temp;
temp = a + 1;
b = temp * temp;
c = temp / 4;

需要注意的是公共子表达式消除可能会带来一些寄存器压力,因为需要定义更多的变量。

死代码消除

编译器在编译时会将不会被执行的代码消除掉,移除这类的代码有两种优点,首先可以减少程序的大小,其次还可以避免程序在运行中进行不相关的运算行为,缩减运行时间。 举例如下:

int a  = 10;
int b = a * 10;
return 0;

优化为:

return 0;

循环展开

编译器在高度优化下,会将循环展开,它可以展开重复次数很低的循环,来避免循环的开销,例如:

int i, a[2];
for (i = 0; i < 2; i++) 
a[i] = i+1;

展开为:

int a[2];
a[0] = 1;
a[1] = 2;

要注意过多的循环展开不一定是好事,因为它占用了代码缓存中太多的空间,同时也增大了程序的体积。

循环不变代码移除

如果循环中的某些运算与循环计数器无关,可以将其移出循环体。例如:

int a[10], b;
for (int i = 0; i < 10; i++) { a[i] = b * b;}

编译器可以改变它为:

int i, a[10], b, temp;
temp = b * b;
for (i = 0; i < 10; i++) { a[i] = temp;}

计算规模缩减

编译器可以使用运算难度小的表达式来替换难度大的表达式,例如:

int a;
int b = a * 32;

转化为:

int a;
int b = a << 5;

llvm提供的pass

llvm框架提供了一系列用于进行代码变换的pass,这些pass工作在ir层面

编译器优化选项

一般我们开发的程序都有两个版本,Debug版本和Release版本,Debug版本是调试版本,该版本通常不带任何优化,发布程序的时候会使用Release版本,该版本会对程序做很多优化。优化有两个方向,一个是优化程序体积,一个优化运行速度,当速度优化到极致时,有可能程序体积不是最小,当程序体积优化到最小时,一般程序执行速度不是最快的。 以clang编译器为例,存在O0、O1、O2、O3等优化选项,O0为默认选项,采取很少的优化措施,一般来说使用O2级别优化即可,从O0到O1、O2、O3随着优化级别的提升,能够得到的提升越来越不明显,甚至可能出现更高级别的优化反而不如低级别优化的情况。