Chapter 26 Code-Tuning Techniques
 代码调整技术

26.1 Logic 逻辑
·Stop testing When You Know the Answer 在知道答案后停止判断
 例如,使用循环找一个数组中的第一个负值,找到后要用break来停止循环。
·Order Tests by Frequency 按照出现频率来调整判断顺序
 安排判断的顺序,让运行最快和判断结果最有可能为真的判断首先被执行。也就是说,让程

序更容易进入常见的处理,如果有低效的情况,那就应该出现在处理非常见的情况时。
 这样的原则适用于case语句以及if-then-else语句串。
·Compare Performance of Similar Logic Structures  相似逻辑结构之间的性能比较
 case和if-then-else语句,考虑到开发环境的差异,任何一种方法都有可能更为高效。
 简而言之,没有什么能替代测量得出的结论啦。
·Substitute Table Lookups for Complicated Expressions 用查询表替代复杂表达式
 第18章“表驱动法”有用查表法替代复杂逻辑的详细说明。
 使用一张查询表会比穿梭在复杂的逻辑链路中更为高效。
·Use Lazy Evaluation 使用惰性求值
 惰性求值 类似于即时完成策略(just-in-time),即仅到工作必须完成的时候才去处理。
 举例:假设你的程序有一张表,里面有5000个值。程序在启动的时候生成这张表,然后在运

行中使用它。如果程序仅仅用到整张表的很小一部分,那么与其在最开始计算表中的全部内容,还不如

到需要的时候计算。一旦某个条目被计算出来,仍然可以把它存放起来,以备后用(即所谓的“缓存”)。

26.2 Loops 循环
· Unswitching 将判断外提
切换(Switching)一词指代循环中的判断,该判断每次循环中都会被执行。
如果循环运行时某个判断结果不会改变,你就可以把这个判断提到循环的外面,从而避免在循环中进行

判断。
for(i = 0; i < count; i++){
 if(sumType == SUMTYPE_NET) {
  netSum += amount[i];
 }
 else{
  grossSum += amount[i];
 }
}
这段代码,循环每次都会反复执行if测试语句,尽管在整个循环中他都取得相同的值。应该重写为:
if(sumType == SUMTYPE_NET) {
 for(i = 0; i < count; i++){
  netSum += amount[i];
 }
}
else{
 for(i = 0; i < count; i++){
  grossSum += amount[i];
 }
}

·Jamming 合并
合并(jamming),或融合(fusion),就是把两个对相同一组元素进行操作的循环合并在一起。此举

所带来的好处就是把两个循环的总开销减少到单个循环的开销。
循环合并 有两种主要风险。首先,要合并的两个循环各自的下标有可能被改变,此后两者将无法共用一

个循环下标。其次,你或许没有那么容易就能把循环合并在一起。合并之前,你要保证合并之后二者相

对于其他代码的先后顺序仍然正确。

·Unrolling 展开
 循环展开的目的是减少维护循环所需要做的工作。

·Minimizing the Work Inside Loops 尽可能减少在循环内部做的工作
 编写高效循环的关键在于尽可能减少循环内部所做的工作。
 这是一种很好的编程实践,在很多情况下还可以改善程序的可读性。
for(i=0;i<rateCount; i++)
{
       netRate[i] = baseRate[i] * rates -> discounts -> factors -> net;
}
这时,把复杂的指针表达式付给一个命名准确的变量,不但可以改善代码的可读性,还能提高代码性能


quantityDiscount = rates -> discounts -> factors -> net;
for(i=0;i<rateCount; i++)
{
       netRate[i] = baseRate[i] * quantityDiscount;
}

·Sentinel Values 哨兵值
当循环条件是一个复合判断的时候,你可以通过简化判断来节省代码运行时间。如果该循环是一个查找

循环,简化方法之一就是使用哨兵值(Sentinel Value)。
你可以把它放到循环范围的末尾,从而保证循环一定能够终止。
--------------------------------------
found = false;
i = 0;
while ((!found) && (i < count)) {
 if(item[i] == testValue ) {
  found = true;
 }
 else{
  i++;
 }
}
if(found) {...}
---------------------------------------
这段代码里,每次循环迭代都要判断!found和i<count。前者判断是否找到了所需的元素;后者避免循

环超出数组的末尾。
循环内部,每个Item又要单独判断。因此,对每一次迭代来说,循环中实际上有3次判断。
---------------------------------------
使用哨兵值来循环:
intialValue = item[count];
item[count] = testValue;
i=0;
while ( item[i] != testValue ) {
 i++;
}
//check if value was found
if( i < count){ ... }
---------------------------------------
在数组的末尾放置哨兵值,存放testValue。使得每次循环只执行一次判断就可以了。
最后如果i没有达到最后末尾的哨兵值,就说明找到需要查找的值了。
事实上,在任何使用线性查找的场合你都可以使用哨兵法——从链表到数组。
需要注意的是你必须仔细阅读哨兵值,并小心地把它放到数据结构中。

·Putting the Busiest Loop on the Inside 把最忙的循环放在最内层
循环嵌套时,需要考虑把哪一个循环放在外面,哪一个放在里面。例如,下面的循环嵌套的例子就有改

进的余地。
for ( column =0; column < 100; column++ ){
 for(row = 0; row< 5; row++){
  sum = sum + table[row][column];
 }
}
改进的关键在于解决外层循环执行的次数远远多于内层循环这一问题。每次执行的时候,循环都要初始

化循环下标,在执行一遍循环代码后将它递增,然后进行检查。
循环所执行的总次数是100次外部循环,100*5即500次内部循环,一共600次。
如果把内外循环交换,那么将会是5次外部循环,5*100即500次内部循环,一共505次。
通过分析,你预期此举能节省下(600-505)/600即16%的时间。实际测量后,更多一些。

·Strength Reduction 消减强度
削减强度意味着用多次轻量级运算(例如加法)来代替一次代价高昂的运算(例如乘法)。
VB示例:乘以循环下标
For i=0 to saleCount -1
 commission(i) = (i + 1) * revenue * baseCommission * discount
Next
这段代码简单明了,但代价有些高。通过累加得到乘积,而不是每次去计算。
VB示例:用加不用乘
incremetalCommission = revenue * baseCommission * discount
cumulativeCommission = incremetalCommission
For i=0 to saleCount - 1
 commission(i) = cumulativeCommission
 cumulativeCommission = cumulativeCommission + incrementalCommission
Next

26.3 Data Transformation 数据变换
· Use Integers Rather Than Floating-Point Numbers
 使用整型数而不是浮点数
整型数的加法和乘法要比浮点数的相应运算快很多。因此,例如把循环下标从浮点类型改为整型这样的

方法能够为代码运行节省很多时间。

·Use the Fewest Array Dimensions Possible 数组维度尽可能少
多年来累积的编程智慧表明,处理多维数组的代价是惊人的。
VB和JAVA语言上优化效果表现明显。但是C++和C#几乎没有优化效果,因此不用对它要求苛刻。

·Minimize Array References 尽可能减少数组的引用
减少对数组的访问总是有好处的。例如在一段代码中,频繁访问array[i],就可以优化为一个局部变量(

array[i]),所有使用array[i]的地方换为这个局部变量。

·Use Supplementtary Indexes 使用辅助索引
使用辅助索引的意思就是添加相关数据,使得对某种数据的访问更为高效。
1. String-Length index 字符串长度索引
 你可以在不同的字符串存储策略中发现辅助索引的身影。在C语言中,字符串是被一个值为0

的字节中止。对VB字符串而言,在每个字符串的开始位置隐藏有一个长度的字节,标示该字符串的长度

。要确定C语言字符串的长度,程序需要从字符串开始位置对各个字节计数,知道发现值为0的字节为止

。要确定VB字符串的长度,程序只需要看看长度字节就可以了。
 VB长度字节就是一个例子,说明给数据结构增加一个索引,有可能让一些特定操作——例如

计算字符串长度——变得更快。
 你可以把这种为长度添加索引的思想应用到任何可变长度的数据类型上面。在需要知道数据

长度的时候,相对于临时计算,提早维护这样的结构长度数据显然更为有效。
2. Independent,Parrllel index Structure 独立的平行的索引结构
 有时,与操作数据类型本身相比,操作数据类型的索引会更为有效。尤其是如果数据类型中

的条目很大或是很难于移动,那么对索引排序和查找会比直接对数据进行相同操作要快。

·Use Caching 使用缓存机制
 缓存机制就是把某些值存起来,使得最常用的值会比不太常用的值更容易被获取。
 除了用来缓存磁盘上的记录,缓存机制还能应用到其他领域。在MS Windows的字体描绘程

序中,一度的性能瓶颈就是在显示每一个字符的时候获取字符的宽度。在将最近所使用的字符宽度缓存

之后,显示速度即提升了大约一倍。
 缓存的成功取决于 访问被缓存元素,创建未缓存元素,以及在缓存中保存新元素等动作相关

的代价。一般而言,如果创建新元素的代价越大,请求相同信息的次数越多,那么缓存就越有价值。 同

样,访问缓存元素或将新元素放到缓存中的开销越小,缓存体现的价值就越大。
 同其他优化技术一样,缓存增加了程序的复杂性,使得程序更容易出错。

26.4 Expressions 表达式
· Exploit Algebraic Identities 利用代数恒等式
 你可以通过代数恒等式,用低代价的操作替代复杂操作。例如:下面两个表达式逻辑上等价
 not a and not b
 not (a or b)
 如果选则第二个表达式,你就避免了一次Not操作。
 尽管避免执行一次not操作节省的时间可能微不足道,但这种普遍的原则是具有强大威力的。
 Jon Bentley曾描述过一个判断sqrt(x) < sqrt(y)的程序。由于只有当x<y的时候,sqrt

(x)<sqrt(y),因此,可以用x<y来替代前面那个判断。由于sqrt()程序的代价很高,你可以预计此举的

效果是激动人心的。

·Use Strength Reduction 削弱运算强度
可能的替代方法:
1. 用加法代替乘法
2. 用乘法代替幂乘
3. 用三角恒等式代替等价的三角函数
4. 用long或者int来代替longlong整数
5. 用定点数或者整型数代替浮点数
6. 用单精度数代替双精度数。
7. 用移位操作代替整数乘2或除2.

·Initialize at Compile Time 编译期初始化

·Be Wary of System Routines 小心系统函数

·Use the Correct Type of Constants 使用正确的常量类型
 所使用的具名常量和应该同被赋值的相应变量具有相同的类型。当常量和相关变量的类型不

一致时,那么编译器就不得不现对常量进行类型转换,然后才能将其赋给变量。

· Precompute Results 预先计算出结果
通过预先计算优化程序可以有如下几种形式:
1. 在程序执行之前算出结果,然后把结果写入常量,在编译时赋值;
2. 在程序执行前计算结果,然后把它们硬编码在运行时使用的变量里;
3. 在程序执行前计算结果,把结果存放在文件中,在运行时载入;
4. 在程序启动时一次性计算出全部结果,每当需要时去引用。
5. 尽可能在循环开始之前计算,最大限度地减少循环内部需要做的工作。
6. 在第一次需要结果时进行计算,然后将结果保存起来以备后用。

·Eliminate Common Subexpressions 删除公共子表达式
 如果发现某个表达式老是出现在你面前,就把它赋给一个变量,然后在需要的地方引用该变

量。

26.5 Routines 子程序
代码调整的利器之一就是良好的子程序分解。
· Rewrite Routines Inline 将子程序重写为内联
 在计算机编程历史的早期阶段,在一些机器中调用子程序就可能严重地影响性能。子程序调

用意味着操作系统需要把程序内存交换出去,换入一个子程序目录,换入特定的子程序,执行子程序,

然后再把这个子程序换出去,最后把调用子程序交换回来。所有的这些交换操作都要吞噬大量的资源,

让程序变慢。
 对今天的计算机来说,调用一个子程序要付出的代价小多了。不会因为你调用了某个子程序

而开出一张罚款单。

26.6 Recoding in a Low-Level Language 用低级语言重写代码
有一句亘古不变的箴言也不能不提:当程序遭遇性能瓶颈的时候,你应当用低级语言重写代码。
如果程序是用C++写的,低级语言或许是汇编;如果Python写的,那么低级语言可能是C。
在低级语言中重新编写代码更有可能改善速度和减少代码规模。
 有一种相对简单有效地汇编重编码方法,即启用一个能顺带输出汇编代码列表的编译器。把

需要调整子程序的汇编代码提取出来,保存到单独的源文件中。将这段汇编代码作为优化工作的基础,

手动调整代码,在接下来每一步工作中检查代码的正确性并量化所取得的改进。
 一些编译器还可以将高级语言的语句作为注释嵌入到汇编代码中。如果你的编译器提供了这

项功能,你可以把高级语言代码留下来,作为汇编代码。

26.7 The More things Change, The More They Stay the Same 变得越多,事情反而越没变
 代码调整无可避免地为性能改善的良好愿望而付出复杂性,可读性,简单性,可维护性方面

的代价。由于每一次调整后需要对性能进行重新评估,代码调整还引入了巨额的管理维护开销。

Key Points 要点
1. 优化结果在不同的语言,编译器和环境下有很大的差异。如果没有对每一次的优化进行测量,你将无

法判断优化到底是帮助还是损害了这个程序。
2. 第一次优化通常不会是最好的。即使找到了效果很不错的,也不要停下扩大战果的步伐。
3. 代码调整这一话题有点类似于核能,富有争议,甚至会让人冲动。一些人认为代码调整损害了代码的

可读性和可维护性,他们绝对会将其弃之不用。其他人则认为只要有适当的安全保障,代码调整对程序

是有益的。如果你决定使用本章的调整方法,请务必谨慎行事。

posted on 2009-05-15 16:27  SamZhang  阅读(825)  评论(0编辑  收藏  举报