摘自:关于iOS开发
一般来说,程序优化主要是以下三个步骤:
- 算法优化
- 代码优化
- 指令优化
算法优化
算法上的优化是必须首要考虑的,也是最重要的一步。一般我们需要分析算法的时间复杂度,即处理时间与输入数据规模的一个量级关系,一个优秀的算法可以将算法复杂度降低若干量级,那么同样的实现,其平均耗时一般会比其他复杂度高的算法少(这里不代表任意输入都更快)。
比如说排序算法,快速排序的时间复杂度为O(nlogn),而插入排序的时间复杂度为O(n*n),那么在统计意义下,快速排序会比插入排序快,而且随着输入序列长度n的增加,两者耗时相差会越来越大。但是,假如输入数据本身就已经是升序(或降序),那么实际运行下来,快速排序会更慢。
1、避免循环内部的乘(除)法以及冗余计算
这一原则是能把运算放在循环外的尽量提出去放在外部,循环内部不必要的乘除法可使用加法来替代等。如下面的例子,灰度图像数据存在BYTE Img[MxN]的一个数组中,对其子块 (R1至R2行,C1到C2列)像素灰度求和,简单粗暴的写法是:
1 int sum = 0; 2 for(int i = R1; i < R2; i++) 3 { 4 for(int j = C1; j < C2; j++) 5 { 6 sum += Image[i * N + j]; 7 } 8 }
但另一种写法:
1 int sum = 0; 2 BYTE *pTemp = Image + R1 * N; 3 for(int i = R1; i < R2; i++, pTemp += N) 4 { 5 for(int j = C1; j < C2; j++) 6 { 7 sum += pTemp[j]; 8 } 9 }
可以分析一下两种写法的运算次数,假设R=R2-R1,C=C2-C1,前面一种写法i++执行了R次,j++和sum+=…这句执行了RC次,则总执行次数为3RC+R次加法,RC次乘法;同 样地可以分析后面一种写法执行了2RC+2R+1次加法,1次乘法。性能孰好孰坏显然可知。
2、避免循环内部有过多依赖和跳转,使cpu能流水起来
关于CPU流水线技术可google/baidu,循环结构内部计算或逻辑过于复杂,将导致cpu不能流水,那这个循环就相当于拆成了n段重复代码的效率。
另外ii值是衡量循环结构的一个重要指标,ii值是指执行完1次循环所需的指令数,ii值越小,程序执行耗时越短。下图是关于cpu流水的简单示意图:
简单而不严谨地说,cpu流水技术可以使得循环在一定程度上并行,即上次循环未完成时即可处理本次循环,这样总耗时自然也会降低。
先看下面一段代码:
1 for(int i = 0; i < N; i++) 2 { 3 if(i < 100) a[i] += 5; 4 else if(i < 200) a[i] += 10; 5 else a[i] += 20; 6 }
这段代码实现的功能很简单,对数组a的不同元素累加一个不同的值,但是在循环内部有3个分支需要每次判断,效率太低,有可能不能流水;可以改写为3个循环,这样循环内部就不 用进行判断,这样虽然代码量增多了,但当数组规模很大(N很大)时,其效率能有相当的优势。改写的代码为:
1 for(int i = 0; i < 100; i++) 2 { 3 a[i] += 5; 4 } 5 for(int i = 100; i < 200; i++) 6 { 7 a[i] += 10; 8 } 9 for(int i = 200; i < N; i++) 10 { 11 a[i] += 20; 12 }
关于循环内部的依赖,见如下一段程序:
1 for(int i = 0; i < N; i++) 2 { 3 int x = f(a[i]); 4 int y = g(x); 5 int z = h(x,y); 6 }
其中f,g,h都是一个函数,可以看到这段代码中x依赖于a[i],y依赖于x,z依赖于xy,每一步计算都需要等前面的都计算完成才能进行,这样对cpu的流水结构也是相当不利的,尽 量避免此类写法。另外C语言中的restrict关键字可以修饰指针变量,即告诉编译器该指针指向的内存只有其自己会修改,这样编译器优化时就可以无所顾忌,但目前VC的编译器似乎不支 持该关键字,而在DSP上,当初使用restrict后,某些循环的效率可提升90%。
3、定点化
定点化的思想是将浮点运算转换为整型运算,目前在PC上我个人感觉差别还不算大,但在很多性能一般的DSP上,其作用也不可小觑。定点化的做法是将数据乘上一个很大的数后,将 所有运算转换为整数计算。例如某个乘法我只关心小数点后3位,那把数据都乘上10000后,进行整型运算的结果也就满足所需的精度了。
4、以空间换时间
空间换时间最经典的就是查表法了,某些计算相当耗时,但其自变量的值域是比较有限的,这样的情况可以预先计算好每个自变量对应的函数值,存在一个表格中,每次根据自变量的 值去索引对应的函数值即可。如下例:
1 //直接计算 2 for(int i = 0 ; i < N; i++) 3 { 4 double z = sin(a[i]); 5 } 6 7 //查表计算 8 double aSinTable[360] = {0, ..., 1,...,0,...,-1,...,0}; 9 for(int i = 0 ; i < N; i++) 10 { 11 double z = aSinTable[a[i]]; 12 }
后面的查表法需要额外耗一个数组double aSinTable[360]的空间,但其运行效率却快了很多很多。
5、预分配内存
预分配内存主要是针对需要循环处理数据的情况的。比如视频处理,每帧图像的处理都需要一定的缓存,如果每帧申请释放,则势必会降低算法效率,如下所示:
1 //处理一帧 2 void Process(BYTE *pimg) 3 { 4 malloc 5 ... 6 free 7 } 8 9 //循环处理一个视频 10 for(int i = 0; i < N; i++) 11 { 12 BYTE *pimg = readimage(); 13 Process(pimg); 14 }
1 //处理一帧 2 void Process(BYTE *pimg, BYTE *pBuffer) 3 { 4 ... 5 } 6 7 //循环处理一个视频 8 malloc pBuffer 9 for(int i = 0; i < N; i++) 10 { 11 BYTE *pimg = readimage(); 12 Process(pimg, pBuffer); 13 } 14 free
前一段代码在每帧处理都malloc和free,而后一段代码则是有上层传入缓存,这样内部就不需每次申请和释放了。当然上面只是一个简单说明,实际情况会比这复杂得多,但整体思想是一致的。
记录分享这些年。