程序的加速通过几种不同的方式得到的。
1.算法和数据结构(选用合适的数据结构)
2.算法优化
3.数据结构重组(必要时重新配置数据结构)
4.代码优化。
5.硬件优化。
尝试利用不同条件,从不同途径获得估算结果,比进行相互验证;
72法则估算指数过程的增长:假定你投入了一笔钱,时间是y年,利率每年是r% 如果r*y = 72,那么大致说来你投入的钱会翻番的。
估算技巧可用实践来检验和提高。
安全系数:
如果我们统计了连续百万次开方运算需要0.2s,那么一次开方运算真的需要200ns么?不是的,实际的运算可能比这慢许多,因为开方函数缓存了一些最近的参数作为计算的起始值;(在第5、9章中各有一个类似的问题,也是考虑了缓存对性能的优化)
在程序的实际使用中可能会发生意想不到的事或由于我们考虑的不全面导致实际使用的速度比估算的速度慢。我们应该尽量低估程序的性能(2或4或6……倍)。以得到较高的可靠性。
本章以一个经典问题说明了算法设计中的常用技巧:在一个长度为n的浮点数向量中,如何找到和最大的连续子向量?也即找到最优的 i,j,使得x(i) + x(i+1) + … + x(j) 最大。
不需要思考的算法( O(n^3)):
将所有子序列(数量为n^2)逐个求和(使用O(n)时间),取其最大。
在求和方式上优化( O(n^2)):
思路依旧为求所有子序列的和,但是求和方式上将做优化。原理是sum(i, j) = sum(i,j-1) + x(j) 。只要保存每个子序列的和就不需要再次求解,此方法使用动态规划的思想。将使用O(n)时间的求和程序降低为O(1).
使用分治法( O(nlogn) ):
将向量分为等长的两段,最大和可能在第一或第二段中,也可能是跨越两段边界的一个子序列。所以比较其左半部分,右半部分和跨越两段部分,分别的最大子序列之大小即可。
float maxsum3(l, u)
if (l > u) //0个元素
return 0 ;
if (l == u) //1个元素
return max(0, x[l]) ;
m= (l + u)/2
//找穿越左右的左半部分的最大和
lmax = sum = 0
for (i = m; i >= l; i--) //所有以中间m为终点的数链和 最大的
sum += x[i]
lmax = max(lmax, sum) ;
//找穿越左右的右半部分的最大和
rmax = sum = 0
for i = (m, u] //所有以中间m为起点的数链和 最大的
sum += x[i]
rmax = max(rmax, sum)
return max(lmax+rmax, maxsum3(l, m), maxsum3(m+1, u))
使用动态规划( O(n) ): x[1,i]的最大子序列为如下两个值的最大值,其一是x[1,i-1]的最大子序列M(i-1),其二为终止于x(i)的最大子序列M_post(i);
这个算法描述起来比较困难,但是看到具体实现比较好理解,所以直接列出实现。
maxsofar = 0//用来记录
maxendinghere = 0//用来计算
for i = [0, n)
//maxendinghere包含了截止于位置i的最大子向量的值
maxendinghere = max(maxendinghere + x[i], 0)
maxsofar = max(maxsofar, maxendinghere) //记录下到i的最大子数组和
提高时间效率技巧小结:
· 保存状态,避免重复计算;
· 预处理数据以便于处理(如O(n^2)中的累加和);
· 分而治之;
· 动态规划(扫描);
· 使用累加数组。
· 了解时间的下界;(例如在本例中,时间下界就是O(n), 所以不用再期待常数时间算法)
代码调优是针对具体问题对代码进行的微调,但有时仍然能够起到令人惊异的优化效果。调优的第一个步骤是对程序时间进行准确的度量,寻找到真正的瓶颈,并力图解决。调优的常见策略如下:
· 简化关键操作。例如,通常取余操作(%)的时间代价很高,在循环数组的访问中,如果把 j = ( j + m ) % N; 改为 j += m; if( j >=N ) j-=N; 化取余为比较和减法,这样通常会有明显的效率提升;当然另一方面,如果m很大,超过了缓存范围,那么程序的瓶颈就转移到了存储器访问上,此时上述调优策略就不再发挥作用了。(Again,我们再次看到了存储器结构对程序设计的影响,类似的例子在第5章和第7章存在)
· 内联函数。简单函数调用写成宏操作会提高效率,但有时也有例外,#define max(a,b) ((a)>(b):?(a):(b)) 中,如果a和b是两个函数调用的话,那么使用宏定义的max时,a和b函数各被调用了两次,这样不仅效率降低,有时甚至会造成错误。更好的方法是使用C++中定义的内联函数。
· 合并测试 & 展开循环。例如在顺序搜索中,
for( i = 0; i < N; i ++)
if( x[i] == t)
return i;
如果写成
x[n] = t;//设置哨兵;
for( i = 0; ; i += 8)
if( x[i ] == t ) { break;}
if( x[i + 1] == t ) { i += 1; break;}
if( x[i + 2] == t ) { i += 2; break;}
……
if( x[i + 7] == t ) { i += 7; break;}
if(i==n) return -1;
else return i;
在这个变化中,首先把 i < N
的测试合并到了 x[i] == t 的测试当中,这样需要进行的比较操作减少了;其次,减少了 i++的自增操作,这里将步长设为8是尝试的结果,实际中可能随存储器结构或其他因素有关;
· 用等价的数学表达式替代复杂数学运算;
在有关二分搜索优化的例子中,综合运用了展开循环、合并测试以及数学表达式替换等多种方式,完成了优化。
空间指占用内存的大小,程序中使用的变量和程序本身都会占用内存。
节省内存有几个方法:
不存储、直接计算(时间换空间)。
为稀疏数据设计特别的数据结构。
数据压缩。
动态分配空间,定期回收垃圾。