如何提高程序效率

一、程序效率

程序效率,是用执行的步骤(step)数――时间复杂度、占内存的多少来衡量的――空间复杂度。完成某项工作,执行的步骤(step)的次数最少、
占用内存最小是程序员所追求的。特别是嵌入式系统的开发,内存等资源都是有限的。
因此,提高效率的着眼点应该是
减少执行次数
减少占用空间

二、效率改善的指导原则

-满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率;
如果程序的正确性、可靠性得不到保证,提高效率就失去了根本;
如果程序的健壮性得不到保证,提高效率就失去了目标;
如果程序的可读性得不到保证,提高效率就失去了方向;

-以提高程序的全局效率为主,提高局部效率为辅;
如何只从局部角度出发,局部效率的改善一般对全局效率改善作用不大,有时甚至影响全局效率的改善;
应该从全局角度出发,整体考虑,作出统一改善的调整,有的时候局部利益为配合整体需要应作出牺牲;

-在优化程序的效率时,应当先找出限制效率的“瓶颈”;
非关键点的改善,不会根本改变效率的现状;
先进行一些基准数据测量和问题收集,寻找提高效率的关键点;
有时一次关键的改动还不能解决问题,还需要进一步的数据测量和持续的改进,直到符合要求;

-先优化数据结构和算法,再优化执行代码;
因为O(n2)的算法写不出O(nlog2n)的程序,因此写程序前应该考虑数据结构和算法的选择和优化;
如果你已经选择了正确的算法,那么只有到了写程序的最后,才有可能去关心执行代码的优化问题;

-时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷;
一般来讲,在空间允许的时候,我们会花费空间换取时间效率的大幅提升;
当空间受限--时间效率和空间对立的时候,我们根据需要,在两者之间作出适当折中;

三、时间效率改善的考虑

-减少内存分配的次数
一次能够申请的内存,就不要多次申请;
被多次访问函数中存储“不变量”的变量请定义成为static。如:

  1. GetLocalHostName(char* name)   
  2. {       
  3.     char funcName[] = "GetLocalHostName";  
  4.   
  5.         sys_log( "%s begin......", funcName );   
  6.     ...   
  7.     sys_log( "%s end......", funcName );   
  8. }   

如果这是一个经常调用的函数,每次调用时都要对funcName进行分配内存,这个开销很大啊。把这个变量声明成static吧,当函数再次被调用时,就会省去了分配内存的开销,执行效率也很好。 
-提高循环体效率
减少循环次数
减少循环体内执行语句的数量
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数
下面例子a和例子b的功能一样,但效率不同
例a:  
  1. for (i=0; i<5;i++)  
  2. {  
  3.     for (j=0; j<100; j++)  
  4.     {  
  5.         Dothing();  
  6.     }  
  7. }  
例b:  
  1. for (j=0; j<100;j++)  
  2. {  
  3.     for (i=0; i<5; i++)  
  4.     {  
  5.         Dothing();  
  6.     }  
  7. }   

-减少指针定位
指针(->)使用很方便,但实际运行往往需要进行地址计算;不必要的地址计算会导致性能的下降;
尽可能减少->的多级嵌套。

-提高数学表达式的效率
a*b + a*c = a*(b+c); 减少一次乘法,但不改变表达式的意义。
b/a + c/a = (1/a)*(b+c); 把两次除法变成一次除法和一次乘法,在几乎所有的平台上,除法都比乘法慢。
(a || b ) && c ) = c && ( a || b ); 当c为假时,第一个表达式要计算( a || b ),第二个表达式则不计算。

四、空间效率改善的考虑

合理结构体成员定义顺序:按照类型从小到大,相同类型连续存放
例a和例b的占用的空间是不同的:
例a
  1. typedef  struct {  
  2.     short   a;  
  3.     int     b;  
  4.     short   c;  
  5. } AA_t;  
例b
  1. typedef struct  
  2.     short   a;  
  3.     short   c;  
  4.     int     b;  
  5. } BB_t   

-冗余数据压缩,可以减少对内存的占用
  如有些矩阵数据的存储。有些矩阵中的非零元素很少,可以考虑通过只存储非零数据来减少对存储空间的占用。

-动态内存使用的方式,可以提高内存的利用率。
  追求空间的效率和追求时间的效率,往往是矛盾的,需要全面权衡。


-减少代码的行数可以减少ROM的占用
   对于嵌入系统而言,ROM资源是很宝贵的。减少代码行数是减少ROM占用量 的有效途径;
   减少代码行的方法:
消除冗余代码可以有效减少代码行数
通过函数指针和函数表可以减少程序代码规模


四、几个实例

效率改善的例A

请理解下面的代码
  1. forint i = 0; i < numPixels;i++ )  
  2. {     
  3.     rendering_context->back_buffer->surface->bits[i] = some_value;   
  4. }  

做如下修改是否更好
  1. unsigned char *back_surface_bits = rendering_context->back_buffer->surface->bits;  
  2. forint i = 0; i < numPixels;i++ )  
  3. {     
  4.     back_surface_bits[i] = some_value;  
  5. }  

还可以进一步修改
  1. unsigned char *back_surface_bits = rendering_context->back_buffer->surface->bits;  
  2. forint i = 0; i < numPixels;i++,back_surface_bits++ )  
  3. {     
  4.     *back_surface_bits = some_value;  
  5. }  

效率改善的例B

问题:有一组处理函数:functionA, functionB,… functionZ,它们的函数形式如下
  1. int functionA(int event)  
  2. int functionB(int event)  
  3. ……  
  4. int functionZ(int event)  

他们分别是不同状态(A,B…,Z)的处理。编写这段处理代码,我们该如何做?
下面代码可行吗?

  1. switch (status) {  
  2.     case A:    functionA(event)  
  3.         break;  
  4.     case B:    functionB(event)  
  5.         break;  
  6.     ……  
  7.     case Z:    functionZ(event)   
  8.         break;  
  9. }  


可行!但是不好!原因是生成目标代码大,而且可维护弱一些。
这么做可以解决上面提到的缺点

  1. typdef  int (*pFunc)(int event);  
  2. typedef enum {   A =0,  
  3.     B,  
  4.     …  
  5.     Z  
  6. } Status_t;  
  7.   
  8.   
  9. pFunc  functionlist[Z] ={     
  10.     functionA,  
  11.     functionB,  
  12.     …  
  13.     functionZ  
  14. };   
  15.   
  16.   
  17. Status_t   status;  
  18. ……  
  19. status被赋值  
  20. ……  
  21. functionlist[status](event);      


这么做是不是更好?

 

 

 

 

 

     1 switch-case 语句。在程序中经常会使用switch-case语句,每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,就浪费了处理器时间。为了提高速 度,可以把具体的情况按照它们发生的相对频率排序。即把最可能发生的情况放在第一,最不可能发生的情况放在最后,这样会减少平均的代码执行时间。
  2 全局变量。使用全局变量比向函数传递参数更加有效率,这样做去除了函数调用前参数入栈和函数完成后参数出栈的需要。当然,使用全局变量会对程序有一些负作用。
  3 嵌入式系统编程应避免使用标准库例程,因为很多大的库例程设法处理所有可能的情况,所以占用了庞大的内存空间,因而应尽可能地减少使用标准库例程。
(1) Inline函数

    在C++中,关键字Inline可以被加入到任何函数的声明中。这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用。 这样做在两个方面快于函数调用。这样做在两个方面快于函数调用:第一,省去了调用指令需要的执行时间;第二,省去了传递变元和传递过程需要的时间。但是使 用这种方法在优化程序速度的同时,程序长度变大了,因此需要更多的ROM。使用这种优化在Inline函数频繁调用并且只包含几行代码的时候是最有效的。
    (2)用指针代替数组

    在许多种情况下,可以用指针运算代替数组索引,这样做常常能产生又快又短的代码。与数组索引相比,指针一般能使代码速度更快,占用空间更少。使用多维数组时差异更明显。下面的代码作用是相同的,但是效率不一样。
    数组索引                指针运算
    For(;;){                p=array
    A=array[t++];           for(;;){
                                 a=*(p++);
    ......                    ......
    }                       }

    指针方法的优点是,array的地址每次装入地址p后,在每次循环中只需对p增量操作。在数组索引方法中,每次循环中都必须进行基于t值求数组下标的复杂运算。
    (3)不定义不使用的返回值

    function函数定义并不知道函数返回值是否被使用,假如返回值从来不会被用到,应该使用void来明确声明函数不返回任何值。

    (4)手动编写汇编

    在嵌入式软件开发中,一些软件模块最好用汇编语言来写,这可以使程序更加有效。虽然C/C++编译器对代码进行了优化,但是适当的使用内联汇编指令可以有效的提高整个系统运行的效率。
    (5)使用寄存器变量

    在声明局部变量的时候可以使用register关键字。这就使得编译器把变量放入一个多用途的寄存器中,而不是在堆栈中,合理使用这种方法可以提高执行速度。函数调用越是频繁,越是可能提高代码的速度。
    (6)使用增量和减量操作符

    在使用到加一和减一操作时尽量使用增量和减量操作符,因为增量符语句比赋值语句更快,原因在于对大多数CPU来说,对内存字的增、 减量操作不必明显地使用取内存和写内存的指令,比如下面这条语句:
    x=x+1;
    模仿大多数微机汇编语言为例,产生的代码类似于:
    move A,x      ;把x从内存取出存入累加器A
    add A,1       ;累加器A加1
    store x       ;把新值存回x

    如果使用增量操作符,生成的代码如下:
    incr x        ;x加1
    显然,不用取指令和存指令,增、减量操作执行的速度加快,同时长度也缩短了。

    (7)减少函数调用参数

    使用全局变量比函数传递参数更加有效率。这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。然而决定使用全局变量会影响程序的模块化和重入,故要慎重使用。
    (8)Switch语句中根据发生频率来进行case排序

    switch语句是一个普通的编程技术,编译器会产生if-else-if的嵌套代码,并按照顺序进行比较,发现匹配时,就跳转到满足条件的语句执行。使 用时需要注意。每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,就把宝贵的处理器时间耗尽。为了提高速度,没法把具体的情况按照它们发生 的相对频率排序。换句话说,把最可能发生的情况放在第一位,最不可能的情况放在最后。
    (9)将大的switch语句转为嵌套switch语句

    当switch语句中的case标号很多时,为了减少比较的次数,明智的做法是把大switch语句转为嵌套switch语句。把发生频率高的case 标号放在一个switch语句中,并且是嵌套switch语句的最外层,发生相对频率相对低的case标号放在另一个switch语句中。比如,下面的程 序段把相对发生频率低的情况放在缺省的case标号内。         pMsg=ReceiveMessage();
        switch (pMsg->type)
        {
        case FREQUENT_MSG1:
        handleFrequentMsg();
        break;
        case FREQUENT_MSG2:
        handleFrequentMsg2();
        break;
        ......
        case FREQUENT_MSGn:
        handleFrequentMsgn();
        break;
        default:                      //嵌套部分用来处理不经常发生的消息
        switch (pMsg->type)
        {
        case INFREQUENT_MSG1:
        handleInfrequentMsg1();
        break;
        case INFREQUENT_MSG2:
        handleInfrequentMsg2();
        break;
        ......
        case INFREQUENT_MSGm:
        handleInfrequentMsgm();
        break;
        }
        }
     如果switch中每一种情况下都有很多的工作要做,那么把整个switch语句用一个指向函数指针的表来替换会更加有效,比如下面的switch语句,有三种情况:
        enum MsgType{Msg1, Msg2, Msg3}
        switch (ReceiveMessage()
        {
        case Msg1;
        ......
        case Msg2;
        .....
        case Msg3;
        .....
        }

     为了提高执行速度,用下面这段代码来替换这个上面的switch语句。

        /*准备工作*/
        int handleMsg1(void);
        int handleMsg2(void);
        int handleMsg3(void);
        /*创建一个函数指针数组*/
        int (*MsgFunction [])()={handleMsg1, handleMsg2, handleMsg3};
        /*用下面这行更有效的代码来替换switch语句*/
        status=MsgFunction[ReceiveMessage()]();

     (10)避免使用C++的昂贵特性

     C++在支持现代软件工程、OOP、结构化等方面对C进行了卓有成效的改进,但在程序代码容量、执行速度、程序复杂程度等方面比C语言程序性能差一些。并 不是所有的C++特性都是肮贵的。比如,类的定义是完全有益的。公有和私有成员数据及函数的列表与一个 struct 及函数原形的列表并没有多大的差别。单纯的加入类既不会影响代码的大小,也不会影响程序的效率。但C++的多重继承、虚拟基类、模板、 异常处理及运行类型识别等特性对代码的大小和效率有负面的影响,因此对于C++的一些特性要慎重使用,可做些实验看看它们对应用程序的影响。

 

 

 

在嵌入式的系统开发中,出于对低价产品的需求, 硬件的设计者需要提供刚好足够的存储器和完成工作的处理能力。所以在嵌入式软件设计的最后一个阶段则变成了对代码的优化。
代码优化的目标是体积小和速度快,可以从算法、数据和指令流三方面来考虑。
3.1 算法优化
大 多数情况下,速度同内存(或者是性能,比如说压缩性能)是不可兼得的。目前程序加速的常用算法一个大方面就是利用查表来避免计算(比如在jpg有 huffman码表,在YUV到RGB变换也有变换表)这样原来的复杂计算现在仅仅查表就可以了,虽然浪费了内存,不过速度显著提升。此外在编写程序时还 要注意提高效率,例如:

3.1.4使用宏函数而不是函数。例如:
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS)
#define BIT_MASK(__bf) (((1U << (bw ## __bf)) - 1) << (bs ## __bf))
#define SET_BITS(__dst, __bf, __val) ((__dst) = ((__dst) & ~(BIT_MASK(__bf))) | (((__val) << (bs ## __bf)) & (BIT_MASK(__bf))))
SET_BITS(MCDR2, MCDR2_ADDRESS, RegisterNumber);
函 数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。函数调用是要使用系统的栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头 会嵌入一些汇编语句对当前栈进行检查;同时,CPU也要在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU时间。而宏 函数不存在这个问题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏函数的时候,该现象尤其 突出。
3.2 Data optimization数据优化
比算法优化层低一级的是数据优化层,我们可以通过改变算法使用的数据类型来优化算法。主要的目的是使处理的数据和目标结构的特性相一致。这项优化不需要大量的代码重写,并独立于算法优化的执行而执行.例如:
3.2.1确定浮点型变量和表达式是 float 型
为 了让编译器产生更好的代码,必须确定浮点型变量和表达式是 float 型的。要特别注意的是,以 ";F"; 或 ";f"; 为后缀(比如:2.718f)的浮点常量才是 float 型,否则默认是 double 型。为了避免 float 型参数自动转化为 double,请在函数声明时使用 float。
3.2.2使用32位的数据类型
编译器有很多种,但它们都包含的典型的32位类型 是:int,signed,signed int,unsigned,unsigned int,long,signed long,long int,signed long int,unsigned long,unsigned long int。尽量使用32位的数据类型,因为它们比16位的数据甚至8位的数据更有效率。
3.2.3明智使用有符号整型变量
在很多情况下,你 需要考虑整型变量是有符号还是无符号类型的。在许多地方,考虑是否使用有符号的变量是必要的。在一些情况下,有符号的运算比较快;但在一些情况下却相反。 比如:整型到浮点转化时,使用大于16位的有符号整型比较快。因为x86构架中提供了从有符号整型转化到浮点型的指令,但没有提供从无符号整型转化到浮点 的指令。在整数运算中计算商和余数时,使用无符号类型比较快。
3.3 Instruction flow optimization指令流优化
第三层优化的目标是低级指令流。比较常见的技术是循环合并(loop merging),循环展开(unrolling),软件流水(software pipelining)。
3.3.1循环合并
如果两个循环计数差不多、循环执行互不相同的操作,可以把它们合并在一起组成一个循环。当两个循环的负荷都不满时,这是非常有用的。
3.3.2循环展开
循环展开就是把循环计数小的循环展开,成为非循环形式的串行程序,或者把循环计数大的循环部分展开,减少循环迭代次数,这样可以节省了用于循环设置、初始化、增加和校对循环计数器的时间。大多数编译器可以自动完成这项工作,手工编译会出现错误代码。
例如:
for( int i = 0; i < 3; i++ ) array[i] = i;
逻辑上等同于:
array[0] = 0; array[1] = 1, array[2] = 2;
3.3.3软件流水
软 件流水是用来安排循环指令,使这个循环多次迭代并行执行的一种技术。在嵌套循环中,编译器仅对最里面的循环执行软件流水,因此对执行周期很少的内循环作循 环展开,外循环进行软件流水,这样可以改进C代码并行执行的性能。使用软件流水还应当注意:尽管软件流水循环可以包含内联函数,但是不能包含函数调用;在 循环中不可以有条件终止指令;在循环体中不可以修改循环控制变量。

 

 

 

转自:http://blog.csdn.net/whz_zb/article/details/7470881

http://blog.csdn.net/qsycn/article/details/4232844

http://www.hzlitai.com.cn/article/ARM-article/example/Code-optimization.html

posted @ 2013-03-10 13:39  wust.zjf  阅读(1138)  评论(0编辑  收藏  举报