|
本帖最后由 冰点 于 2012-12-31 13:56 编辑
优化是一件非常重要的事情。作为一个程序设计者,你肯定希望自己的程序既小又快。DOS时代的许多书中都提到,“某某编译器能够生成非常紧凑的代码”,换言之,编译器会为你把代码尽可能地缩减,如果你能够正确地使用它提供的功能的话。目前,Intel x86体系上流行的C/C++编译器,包括Intel C/C++ Compiler, GNU C/C++ Compiler,以及最新的Microsoft和Borland编译器,都能够提供非常紧凑的代码。正确地使用这些编译器,则可以得到性能足够好的代码。
但是,机器目前还不能像人那样做富于创造性的事情。因而,有些时候我们可能会不得不手工来做一些事情。
使用汇编语言优化代码是一件困难,而且技巧性很强的工作。很多编译器能够生成为处理器进行过特殊优化处理的代码,一旦进行修改,这些特殊优化可能就会被破坏而失效。因此,在你决定使用自己的汇编代码之前,一定要测试一下,到底是编译器生成的那段代码更好,还是你的更好。
本章中将讨论一些编译器在某些时候会做的事情(从某种意义上说,本章内容更像是计算机专业的基础课中《编译程序设计原理》、《计算机组成原理》、《计算机体系结构》课程中的相关内容)。本章的许多内容和汇编语言程序设计本身关系并不是很紧密,它们多数是在为使用汇编语言进行优化做准备。编译器确实做这些优化,但它并不总是这么做;此外,就编译器的设计本质来说,它确实没有义务这么做——编译器做的是等义变换,而不是等效变换。考虑下面的代码:
// 程序段1
int gaussianSum(){
int i, j=0;
for(i=0; i<100; i++) j+=i;
return j;
}
好的,首先,绝大多数编译器恐怕不会自作主张地把它“篡改”为
// 程序段1(改进1)
int gaussianSum(){
int i, j=0;
for(i=1; i<100; i++) j+=i;
return j;
}
多数(但确实不是全部)编译器也不会把它改为
// 程序段1(改进2)
inline int gaussianSum(){
return 5050;
}
这两个修改版本都不同于原先程序的语义。首先我们看到,让i从0开始是没有必要的,因为j+=i时,i=0不会做任何有用的事情;然后是,实际上没有必要每一次都计算1+...+100的和——它可以被预先计算,并在需要的时候返回。
这个例子也许并不恰当(估计没人会写出最初版本那样的代码),但这种实践在程序设计中确实可能出现。我们把改进2称为编译时表达式预先计算,而把改进1成为循环强度削减。
然而,一些新的编译器的确会进行这两种优化。不过别慌,看看下面的代码:
// 程序段2
int GetFactorial(int k){
int i, j=1;
if((k<0) || (k>=10)) return -1;
if((k<=1)) return 1
for(i=1; i<k; i++) j*=i;
return j;
}
程序采用的是一个时间复杂度为O(n)的算法,不过,我们可以把他轻易地改为O(1)的算法:
// 程序段2 (非规范改进)
int GetFactorial(int k){
int i, j=1;
static const int FractorialTable[]={1, 1, 2, 6, 24,
120, 720, 5040, 40320, 362880, 3628800};
if((k<0) || (k>=10)) return -1;
return FractorialTable[k];
}
这是一个典型的以空间换时间的做法。通用的编译器不会这么做——因为它没有办法在编译时确定你是不是要这么改。可以说,如果编译器真的这样做的话,那将是一件可怕的事情,因为那时候你将很难知道编译器生成的代码和自己想的到底有多大的差距。
当然,这类优化超出了本文的范围——基本上,我把它们归入“算法优化”,而不是“程序优化”一类。类似的优化过程需要程序设计人员对于程序逻辑非常深入地了解和全盘的掌握,同时,也需要有丰富的算法知识。
自然,如果你希望自己的程序性能有大幅度的提升,那么首先应该做的是算法优化。例如,把一个O(n2)的算法替换为一个O(n)的算法,则程序的性能提升将远远超过对于个别语句的修改。此外,一个已经改写为汇编语言的程序,如果要再在算法上作大幅度的修改,其工作量将和重写相当。因此,在决定使用汇编语言进行优化之前,必须首先考虑算法优化。但假如已经是最优的算法,程序运行速度还是不够快怎么办呢?
好的,现在,假定你已经使用了已知最好的算法,决定把它交给编译器,让我们来看看编译器会为我们做什么,以及我们是否有机会插手此事,做得更好。
5.1 循环优化:强度削减和代码外提
比较新的编译器在编译时会自动把下面的代码:
for(i=0; i<10; i++){
j = i;
k = j + i;
}
至少变换为
for(i=0; i<10; i++);
j=i; k=j+i;
甚至
j=i=10; k=20;
当然,真正的编译器实际上是在中间代码层次作这件事情。
原理 如果数据项的某个中间值(程序执行过程中的计算结果)在使用之前被另一中间值覆盖,则相关计算不必进行。
也许有人会问,编译器不是都给咱们做了吗,管它做什么?注意,这里说的只是编译系统中优化部分的基本设计。不仅在从源代码到中间代码的过程中存在优化问题,而且编译器生成的最终的机器语言(汇编)代码同样存在类似的问题。目前,几乎所有的编译器在最终生成代码的过程中都有或多或少的瑕疵,这些瑕疵目前只能依靠手工修改代码来解决。 |
|