浅析代码优化——编译器优化原理

时间:2022-01-15 03:54:31

开篇

相信有过编码经验的人都知道,程序的正常运行,只是最基本的要求。更多的,还要考虑程序的性能,运行效率,组织结构,和重用性等等。

今天将简单的讨论一下如何优化程序性能。

要写出高效的程序,可能多数初学者想到的是在程序中用合适的算法和数据结构。这确实是一中提高程序性能的主要方法。

而这里要讨论的是另一种方法,也是很多人都忽略但确实很重要的方法。也是我们这篇文章的主题:

如何编写出编译器能有效优化的源代码。

编译器优化的局限性

 没有万能的东西,编译器也一样。现代编译器都会对源代码进行优化,以提高程序的性能。比如linux下的GCC编译器就能控制优化的等级,优化等级高,对应的程序性能好。对于给定的代码,编译器并不能保证能得到最好的性能,它也有局限性。所以才需要程序员能写出编译器易于理解和优化的代码。

编译器简单的说是讲程序语言代码翻译为机器码,既然是翻译,就不能改变程序想要表达的意思。所以编译器对程序总是小心的使用安全的优化。也就是说:优化后的版本和未优化的版本有一样的行为。

下面简单说一下编译器的局限性

1、存储器别名引用

存储器别名引用就是两个不同的指针可能指向存储器中的同一个位置。

例子说明:看如下的代码

浅析代码优化——编译器优化原理

两个程序似乎有相同的行为。都是将存储在*yp处的值两次加到*xp处存储的位置。这时,twiddle2的效率会更高一些。(可以认为第二个是第一个的简单优化)

第一个函数需要6次存储器引用(读*xp两次,写*xp两次,读*yp两次)而第二个函数只需3次存储器引用(读*yp两次,写*xp一次)

当然上面讨论的是在*xp和*yp指向不同位置的基础上。

现在考虑*xp和*yp指向同一位置的情况:

函数twiddle1的结果是*xp的值翻四倍,twiddle2得到的是3倍。编译器并不知道twiddle1会如何被引用,即不知道*xp,*yp是否指向存储器的同一位置,如果指向同一位置,编译器就不能把函数1优化为函数2的形式。因为他们有不同的行为。

为了编译后程序行为不被改变,也就是所谓的安全优化,编译器只能假设*xp 、*yp 会指向相同的位置。也就是说编译器不会把函数1优化为函数2的形式。这造成了一个妨碍优化的因素。

2、函数副作用

用例子说明问题,看下面简单的代码:

浅析代码优化——编译器优化原理

简单的看两个函数能产生相同的结果。同样的,可以暂时认为func2是func1的优化版本。

但是func2 调用了f()一次,而func1调用了f()两次。如果他们调用的函数f()修改了全局变量,结果就会有所不同

考虑如下f()代码:

int f()

{

  return counter++;

}

这个函数修改了全局变量counter,函数调用的次数会改变程序的行为。也就是说:这个函数有副作用。

大多数编译器在代码优化的时候不会试图判断一个函数是否有副作用。为了安全的优化,编译器会认为所有函数都有副作用。

这也成为妨碍编译器优化的另一因素。

对于函数会有副作用的情况,在写代码的时候就要“帮助”编译器做出判断。和上面的代码对应的,如果函数f()没有副作用,在写程序的时候就把代码写成func2的形式。因为编译器是不会把func1()优化为func2()形式的。

消除循环的低效率

请看下面的代码:

浅析代码优化——编译器优化原理

看for循环里,里面的判断条件i<vec_length(v) (不用去管这个函数是干什么用的)我们知道,for循环每次都要判断 i 的值是否满足条件,按照上面的代码也就意味着每次循环都要执行函数vec_length(v)。如果这个函数的值不会因为循环而改变,那么把这个求值过程放在循环外面,只执行一次函数就把值保存起来,会有更好的效果。

改进后的代码如下:

浅析代码优化——编译器优化原理

这是一个常见的代码优化例子,称为代码移动。适用于要执行多次(如在循环里)但计算结果不变的情况。而这类优化是编译器不能达到的。

编译器会非常小心,为了安全,它认为所有调用的函数都有副作用。

消除不必要的存储器引用

考虑下面的代码:

浅析代码优化——编译器优化原理

看被圈起来的部分,OPER表示某种操作,for循环的目的是将数组data[]中的所有值依次执行某种操作并把最后的值存入*dest。

观察循环内的代码,下面列出每次循环对存储器的操作:

1、读*dest

2、读data[i]

3、(通过计算)写*dest

熟悉汇编的可以参看以上代码的汇编形式:

 

浅析代码优化——编译器优化原理

对于第 i 次循环,第 i 次读的*dest的值刚好就是第 i-1 次写入*dest的值。

由此可见,每次对于*dest的写操作是多余的。因为下一次又会对他写入覆盖前面的值,而我们需要的只是最后一次写入。

考虑到这里,可以用一个临时变量来记录*dest 的值,这样就不用每次循环都重复对*dest的读写工作。在循环结束后讲临时变量的值写入*dest即可。

 如上所说,引入临时变量x的代码如下

浅析代码优化——编译器优化原理

对应的汇编代码:

浅析代码优化——编译器优化原理

小记

 本文先讨论了编译器在代码优化方面的局限性,如简单的存储器别名引用和函数副作用。为了安全的优化,编译器总是考虑最糟的情况,他会认为所有的存储器引用都会有别名引用,所有的函数都会有副作用。而这两点成了限制编译器优化能力的很大因素。所以我们就有责任编写出编译器易于优化的代码。当确定了存储器没有别名引用时,或者当确定函数没有副作用时,适当的修改代码,能协助编译器编写出性能好的程序。