编码中的一些优化技巧

来源:互联网 发布:vc6.0连接mysql数据库 编辑:程序博客网 时间:2024/05/29 04:47

编码中的一些优化技巧

减少指令数

1.降低数据精度

小数点后面位数越多,精度越大。100.11比100.1更加精确。越是精确的数据,所用的位数越多。运算时间越长。浮点数有双精度和单精度之分,单精度浮点数占32bit,双精度浮点数占64bit,处理双精度数据自然要比单精度数据慢。

在C语言中,fabsf()是计算单精度浮点数绝对值的函数,而fabs()是计算双精度浮点数绝对值的函数。如果数据是单精度浮点数,使用fabsf()要比fabs()快。如果一个数据能够使用单精度表示,就不需要使用双精度表示。

 

2.减少函数的调用

函数调用会带来额外的开销,除了引起跳转外,还会产生额外的指令。函数调用是这样的,要进行参数压栈出栈、寄存器的保存、指令跳转等多个步骤,如果程序的性能要求较高,就可以把较小的函数直接转换为代码。

1)将函数直接写出语句

eg: int min(int a, int b){return  a<b? a:b;}c =min(a,b)

直接改写为:

c = a< b? a:b;

2) 将小函数写出宏

#defineMIN(a,b)   ((a) < (b) ? (a) : (b))c =MIN(a,b);

3) 将函数声明为内联函数。

如果嫌宏太麻烦,可以将函数声明为inline函数,编译器将会自动用函数体覆盖函数调用。

inline int min(int a, int b){       return a < b?a :b;}c = min(a,b); //编译器将优化为 c = a<b?a:b;

3.空间换时间

Fibonacci序列实现的例子中:

eg1:int f(int n){       if(n <= 1)              return 1;else       return f(n-1)+f(n-2);}int main(){       intresult;       int i;       for(i= 0;i < 40;i++)       {              result= f(i);              printf(“%d\n”,result);      }}eg2:int arr[40];int f(int i){       intresult;       if(n<=1)              return1;       else{     resultarr[n-1]+arr[n-2]);}arr[n] = result;return  result;}int main(){       intresult;       int i;       for(i= 0;i < 40;i++)       {              result= f(i);              printf(“%d\n”,result);       }} 

eg1在电脑上耗时21秒。而eg2耗时不到1秒。eg2使用了数组将F的值缓存起来,这样计算F(n) = F(n-1)+F(n-2)的时候不在执行递归,直接从数组中取值即可。典型的空间换时间策略。

减少处理器不擅长的操作

单周期指令是处理器最喜欢的,不仅执行时间短,而且有利于流水线。加、减、逻辑运算都是单周期指令,乘、除、分支指令、浮点指令、内存存取指令等,都需要较多的时钟周期。编程的时候,尽可能的少用执行周期长的指令。

1.少用乘法

定点乘法在DSP中需要两个Cycle,而移位操作只需要一个Cycle。如果一个数是乘以2的N次方,就可以用移位代替乘法。

len = len *4;

可以改写为:

len = len<< 2;

2.少用除法和取余

除法和取余操作,将消耗大量时间,很多处理器没有相应的指令,是通过软件实现的。所以应该尽量少用。如果是除以一个常识,eg:

f = f /5.0

可以将它改写为乘法操作:

#define cof 1.0/5

f = f * cof

假如a数据范围为[128,511], b数据范围为18位有效位,当计算b/a的结果时,我们可以把a的数据范围映射到一个整数范围(范围可大可小?),然后与b相乘,然后再移位,使得最后的数据位与其原本b/a的数据位相等。比如我们可以把a通过tmp = 65535/(a-128),映射到[257,511]。即最后的结果把除法运算转换为乘法运算,b/a = (b*tmp) >> 16,右移16bit确保最后的数据位与原来的b/a的结果数据位相同。

3.在精度允许的情况下,可以将浮点数定点化

浮点指令要比定点指令慢很多,功耗也大。在精度不那么高的情况下,就可以将浮点数定点化。用定点指令代替浮点指令。

eg:alpha混合实例

利用两张半透明的图像混合实现alpha混合效果。混合后的图像中每个像素颜色值为:

Pixel_C =(int)(Pixel_A* alpha + Pixel_B*(1-alpha));

alpha为透明度,介于0到1之间的小数。这条语句是浮点运算,如果每个像素都经过这样的运算,是相当耗时的。其实可以将alpha定点为0到32之间的一个整数。改写后为:

Pixel_C =(int)(Pixel_A* alpha + Pixel_B*(32-alpha)+16)>>5;


在图像处理中,RGB转换YUV中常用到这种技术。//BT 601Y = ((((306* R +601*G + 117*B)>>10)-Y_Shift)*Y_Contrast)>>6;Y = CLIP(Y, 0, 1023);U= (-173* R -339*G + 512*B+512)>>10 ;U = CLIP(U, -512, 511);V= (512 * R - 428*G -84*B+512)>>10;V = CLIP(V, -512,511);       本来RGB转YUV时,应该是RGB三通道的值分别乘以相应的小数。在以上的算法中,把相应的小数乘以了1024,变成定点整数。执行时间要大幅度减少。由于ARM是精简指令集的处理器,没有专门计算浮点的硬件支持,它是在浮点模拟器中进行,因此,速度较慢。

 

4.尽量减少分支

现在的处理器都是流水线结构,if 和switch语句会带来跳转,而跳转将打乱流水线的正常执行,影响程序的执行效率。

下面的代码把奇数赋一个值,把偶数赋一个值。

for(i=0;i < 100;i++){       if(i %2 == 0)              a[i]= x;       else              a[i]= y;}

改写如下形式会更好些:

for(i=0;i< 100;i++){       a[i] = x;       a[i+1]= y;}


5.将最可能进入的分支放到if中,而不是else中

Intel处理器有预测分支单元,第一次进入一个分支的时候,由于没有历史信息可供参考,是否跳转取决于Static Predictor(静态预测器)的预测策略。通常静态预测器的预测策略是:向下跳转预测为不跳转,向上跳转预测为跳转。根据此特性,if语句也是需要按照这种方式去编码。

int a = -5;int b = 0;if(a > 0)       b= 1;else       b= -2;

优化内存访问

CPU的执行速度越来越快,但是内存的访问速度却增长缓慢。所有数据和程序必须放入内存才可以工作。CPU计算的时候,需要从内存中读取相应的数据和程序,但是内存的访问速度太慢,严重影响了计算机的执行效率。为了弥补内存速度低下的问题,处理器内部会放置一些SRAM做Cache(缓存),来提高处理器访问程序和数据的速度。Cache作为内核和内存的桥梁如下图所示:

Cache有两个重要的性质:

1)时间局限性(Temporal Locality):如果某个数据被访问,那么不久的将来它很可能再次被访问。最典型的例子就是循环。循环体代码被处理器重复执行,知道循环结束。如果将循环体代码放到Cache中,那么只需要第一次读取改代码需要耗时,以后这些代码每次都能够被内核快速访问,节约了时间。

2)空间局限性(Spatial Locality):如果某项数据被访问,那么与它相邻的数据很可能很快就被访问。典型的例子就是数组。我们一次将数组中的多个数据从内存复制到Cache中,虽然访问第一个元素需要花费点时间,但是后续的元素访问就很快了。这是Cache在空间局限性上的应用。

 

1.少用数组,少用指针

由于大块数据将放到存储器中,简单局部数据将放到寄存器中,因此,尽可能的少用数组和指针,多用简单局部变量。

下面这段代码,需要4次内存访问

c = a [i]*b[i];

d = a[i]+b[i];

如果改写如下形式,就只需要两次内存访问

x = a[i];

y = b[i];

c = x*y;

d = x+y;

 

2.少用全局变量

全局变量由于需要被多个模块使用,不会放到寄存器中,局部变量才能被放到寄存器中。应该尽可能的少使用全局变量。

int x;int fun_a(){       inty,z;       y= x;       z= x+1;              …….}

最好改写为:

int xint fun_a(){       inty,z,temp;       temp=  x;       y= temp;       z= temp+1;       ……}


3.数据对齐访问

对于32bit处理器,一个int型变量i在内存中占据2、3、4、5这四个byte位置

内存访问这个数据的时候,会从0开始的4个byte读入到寄存器A中,再将从4开始的4个byte读入到寄存器B中,然后再将有效数据拼成一个int数据,放到寄存器C中。这种方式访问效率低下。如果变量i存储的位置在从0开始的4个byte处,那么i就可以一次读入到寄存器中。这就是字节对齐与不对齐的差别。对于2byte的变量,它的地址应该为2的整数倍,同理,对于4byte和8字节的变量,它们的起始地址应该分别为4的整数倍和8的整数倍,这样访问速率才会高。

 

4.程序和数据访问符合Cache的时间和空间局部特性

为了使Cache访问效率最高,程序和数据的组织,也应该符合两个特性。典型的例子就是二位数组的访问。

如果a[i][j]在Cache中,那么a[i][j+1]就很可能在Cache中,而a[i+1][j]则不一定。如果代码这样写,效率并不高。

for( j = 0;j <500;j++){       for(I = 0; i< 500;i++)              {                     sum += a[i][j];              }}

改写代码如下后,效率将提高很多。

for( i= 0; i <500; i ++){       for(j= 0; j< 500;j++)              {                     sum += a[i][j];              }}

资料:

《大话处理器》---处理器基础知识读本 万木杨著

0 0