程序优化

摘自:关于iOS开发


一般来说,程序优化主要是以下三个步骤:

  1. 算法优化
  2. 代码优化
  3. 指令优化

算法优化

算法上的优化是必须首要考虑的,也是最重要的一步。一般我们需要分析算法的时间复杂度,即处理时间与输入数据规模的一个量级关系,一个优秀的算法可以将算法复杂度降低若干量级,那么同样的实现,其平均耗时一般会比其他复杂度高的算法少(这里不代表任意输入都更快)。

比如说排序算法,快速排序的时间复杂度为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,而后一段代码则是有上层传入缓存,这样内部就不需每次申请和释放了。当然上面只是一个简单说明,实际情况会比这复杂得多,但整体思想是一致的。


记录分享这些年。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值