1.4 以底层语言思考
Java在上世纪90年代末期逐渐流行开来后,我们常常听到如下的抱怨:
Java的解释性代码逼着我写软件时要注意许多东西:我不能用C/C++中的线性查找,而只能用二分查找算法——这种算法虽好但实现也更麻烦。
类似牢骚真实地反映了使用优化型编译器时存在的主要问题——它们让程序员变得懒惰。尽管优化型编译器在过去几十年突飞猛进,但谁也不可能摆平那些写得糟糕的高级语言源代码。
许多高级语言程序员很天真,想当然地以为优化算法如何如何地厉害,不管给编译器塞入什么东西都能得到高效的代码。这种态度有个问题:尽管编译器是能做一大堆工作,将写得好的高级语言代码转换为高效的机器码,但送入糟糕的源代码,让优化算法乱套照样容易得很。事实上,不止一个C/C++程序员吹嘘其编译器多么能干,他们从没意识到由于其程序实在不敢恭维,编译器的活干得有多差。问题在于,他们从未看一眼编译器根据其程序代码所生成的机器码。他们盲目地以为编译器会干得很好,因为他们听说“编译器产生的代码几乎和汇编语言高手写得一样出色。”
1.4.1 编译器生成的机器码只会与送入的源代码质量一样
编译器不会为了改善软件的性能而去修改算法,这是毋庸置疑的。例如,如果使用线性查找而非二分查找,别指望编译器替你换成更好的算法。当然,优化器也许能加快线性查找速度,比如使代码速度快两到三倍,但这种改进与采用更好的算法不堪一比。事实很容易证明,对于足够大的数据库,解释器的二分查找即便未经优化,仍比顶尖编译器采用线性查找算法快得多。
1.4.2 协助编译器生成更好的机器码
假设我们为应用程序选定了尽可能好的算法,并且不惜血本购置了最好的编译器。要想编写出有效率的高级语言代码,还有什么要做吗?是的!我们还得做一些事情。
编译器业界的一个秘密就是大多数编译器的评测分数都有作弊之嫌。现存编译器的评测分数多数都基于某种指定要用的算法,但算法具体怎样实现则由编译器厂商在其特定的语言中决定。既然编译器厂商通常知道其编译器在送入特定代码序列时会表现如何,所以为了产生尽可能好的可执行代码,他们能够写出相应的高级语言代码序列。
有人会觉得这是欺骗行为,其实还不算。如果编译器在一般条件下——即没有为了提高评测分数而专门开发代码生成技巧——能生成同样的代码序列,炫耀编译器的性能就无可厚非了。既然编译器厂商能采取一些小窍门,你也能啊。在高级语言代码中,通过仔细选择所用的语句,可以“手工优化”编译器产生的机器码。
手工优化有若干级别。在最抽象的层,可以为软件选择更好的算法来优化程序,其技术与特定的编译器和语言无关。
抽象级别再低一层,就是基于所用的高级语言来优化代码,优化不依赖于语言的特定实现。这样的优化或许无法用到其他语言,但适用于该语言的不同编译器。
再下一级,可以考虑代码的组织,优化只对某个厂商的编译器,或者只对某编译器的特定版本有用。
最低一级,得考虑编译器发出的机器码,从而调整我们在高级语言中的语句写法,促使编译器生成希望的机器指令序列。Linux内核就是采用这种办法的例子。据说内核开发者为了控制GCC编译器所产生的80x86机器码质量,而不断调整其C语言代码。
尽管这种开发过程近乎夸张,但有一点肯定无疑:程序员采用这个过程后,将能生成尽量好的机器码。这种代码可以与汇编语言程序员生成的代码平分秋色;程序员在争论编译器产生代码与手工汇编的性能时,这种编译器的输出才是他们该吹牛的地方。事实上,大部分人都不会这么极端,其高级语言代码从来都没有达到这种境界。然而还有个事实是,倘若精心用高级语言编写,程序可以基本达到严谨汇编代码的效率。
那么编译器生成的代码能和汇编语言高手写的代码旗鼓相当吗?正确答案是“否”。不过,认真的程序员以高级语言(比如C语言)所写的代码——如果其代码行为便于编译器转换为高效机器代码——效率可以逼近后者。所以,真正的问题在于“我该如何编写高级语言代码,使编译器转换为尽量高效率的代码?”嗯,回答这个问题正是本书的目标。要是用一句话回答,就是“以底层语言思考,用高级语言编程”。我们来看看怎样做到。
1.4.3 在用高级语言编程时,如何以汇编语言思考
高级语言编译器将语言中的语句转换为一条或多条机器语言(即汇编语言)指令的序列。应用程序占用的地址空间和花在执行上的时间,与机器指令数、编译器发出的机器指令类型息息相关。
然而在高级语言中用不同方法取得同样结果,并不是意味着编译器对每种方法产生的指令是一样的。多数入门性的编程文章指出if-elseif-else语句等效于switch/case语句。
我们来分析下面的简单C程序:
switch( x )
{
case 1:
printf( "X=1\n" );
break;
case 2:
printf( "X=2\n" );
break;
case 3:
printf( "X=3\n" );
break;
case 4:
printf( "X=4\n" );
break;
default:
printf( "X does not equal 1, 2, 3, or 4\n" );
}
/* 等价的if语句 */
if( x == 1 )
printf( "X=1\n" );
else if( x== 2 )
printf( "X=2\n" );
else if( x==3 )
printf( "X=3\n" );
else if( x==4 )
printf( "X=4\n" );
else
printf( "X does not equal 1, 2, 3, or 4\n" );
尽管这两段代码句法上等效,它们能计算出同样的结果,然而不能保证编译器会为这两个例子生成同样的机器指令。
哪个更好呢?如果我们不清楚编译器如何把这些语句转换成机器码,对各种机器指令的效率差异又不甚了解,是无法把握从而明智地选择代码序列的。程序员要是对编译器转换这两个序列的机理成竹在胸,就会根据他们期望编译器生成什么质量的代码,而做出明智的选择。
通过以底层语言思考,用高级语言编程,程序员就能协助编译器达到手工优化的汇编语言代码的质量。不过令人沮丧的是,其负面形式通常也成立:假如程序员不考虑其高级语言代码的底层结果,编译器几乎不可能生成尽量好的机器码。






