小细节大问题——程序的效率

能写出稳定高效的程序一直是程序员所追求的,今天就和大家一起探讨一下关于C++程序优化的几点

 

    由于C/C++语言的复杂性,致使C++编译器隐藏了层层幔布,我们不经意的一条语句都可能是编译器幕后几经周折的结果,在要求程序高效运行的环境下,每一条语句都会让我们慎之又慎,而程序优化又是个十分广泛的话题,包括程序架构设计的优化,语言本身的优化,编程技巧和策略等等,如此大的范围非我能力所及,这里谈的优化就是在实际开发中遇到的问题。

    一.  举手之劳的小差别

    既然说优化就一定要仔细,不放过任何微小的细节,先看个小例子

    1.  int a, i = 5;

         a = i>>1;  (1)

         i = 5;

         a = i/2;     (2)

    语句(1)和语句(2)都是将i的数值除2取整,但实现方式却存在较大的差别。

    (1)  a = i>>1  相当于

           sar        eax, 1      //算术右移一位

    (2)  a = i/2     相当于

           cdq                       //将EDX所有值设置为EAX高位的值

           sub       eax, edx

           sar        eax, 1

    可以看到语句(2)完成同样功能却比语句(1)多出两条指令。需要注意的是,当被除数i是正数时,两者数值相同,因两种写法均向上取整,当i为负数时却不一样,因为语句(2)向上取整,即 i=-5时,语句(2) a=-2而语句(1) a=-3。

    正所谓勿以善小而不为,勿以恶小而为之啊,优化是大局观,但也别忽视小问题。

    二.  猜猜哪一个更快

    写法(1)

    int a[] = {1, 2, 100, 256};

    for(int i=0; i<sizeof a/sizeof a[0]; i++)

        if(a[i]==50)

        {

            printf("for 匹配到50!/n");

        }

    写法(2)                                                           写法(3)

    int  n = 50;                                                    int  n = 50;

    switch(n)                                                       switch(n)

    {                                                                   {

        case 1:                                                          case 1:

        printf("switch匹配到1!/n");                                 printf("switch匹配到1!/n");

            break;                                                           break;

        case 2:                                                          case 2:

        printf("switch匹配到2!/n");                                 printf("switch匹配到2!/n");

            break;                                                           break;

        case 100:                                                      case 100:

        printf("switch匹配到100!/n");                             printf("switch匹配到100!/n");

            break;                                                           break;

        case 256:                                                     case 255:

        printf("switch匹配到256!/n");                             printf("switch匹配到255!/n");

            break;                                                           break;

        default:                                                       default:

            break;                                                           break;

    }                                                                   }

    大家可以先不看结论,自己思考一下看哪个执行效率最高,为什么这样认为,什么原因导致的。

    好了,让我们看一看,凭感觉写法(1)应该最慢,因为是逐个比较。写法(2)和写法(3)实在是看不出有任何差别,效率应该是一样的,所以写法(1)最慢,写法(2)和写法(3)效率一样,等一等,真的是这样吗?我们反汇编看一下(注:本文举例程序均在VC6.0下编译)。

    写法(2)反汇编后类似如下代码:

switch(n)

{

004030F4  mov         eax,dword ptr [ebp-8]                 // [ebp-8] =50              

004030F7  mov         dword ptr [ebp-20h],eax             // [ebp-20h] = 50             

004030FA  cmp         dword ptr [ebp-20h],64h           // 与分支100进行比较          

004030FE  jg             00403114                                    // 若大于100,跳向00403114处      

00403100  cmp         dword ptr [ebp-20h], 64h           // 与分支100进行比较         

00403104  je             00403151                                    //跳转到分支100处理程序处                  

00403106  cmp         dword ptr [ebp-20h],1               //与分支1进行比较                  

0040310A  je             0040311f                                   //跳转到分支1处理程序处                  

0040310C  cmp         dword ptr [ebp-20h],2              //与分支2进行比较                  

00403110  je             00403138                                   //跳转到分支2处理程序处         

00403112  jmp          00403181                                  //跳出switch                               

00403114  cmp         dword ptr [ebp-20h],100h        //与分支256进行比较              

0040311B  je            0040316a                                   //跳转到分支256处理程序处                   

0040311D  jmp         00403181                                 //跳处switch

case 1:

printf("switch跳转到1!/n");

0040311F   mov         esi,esp

00403121   push        offset string "switch/xcc/xf8/xd7/xaa/xb5/xbd1!/n" (004161e0)

00403126   call         dword ptr [__imp__printf (0041849c)]

0040312C   add         esp,4

0040312F   cmp         esi,esp

00403131   call        _chkesp (00402e5a)break;

break;

004015EB   jmp         00403181  //跳出switch

case 2:

printf("switch跳转到2!/n");  //  与上面代码类似故省略.

….

}                                          // switch  结束   

00403181: …             

    看到这里相信大家已经知道了,写法(1)最慢的结论并不一定正确,是有条件的,因为写法(2)同样是逐个比较,只是比较的顺序做了调整,先与分支中第二大的数做比较,在比较不成功时减少了比较次数,在比较成功的情况下二者在效率上是相似的。那写法(3)是否也一样呢?接着看,写法(3)反汇编后代码如下:

   

switch(n)

{

004030F4   mov        eax,dword ptr [ebp-8]            // [ebp-8] =50

004030F7   mov        dword ptr [ebp-20h],eax        //[ebp-20h]=50

004030FA   mov        ecx,dword ptr [ebp-20h]       //ecx = 50

004030FD   sub        ecx,1                                        //ecx =49

00403100   mov        dword ptr [ebp-20h],ecx         //[ebp-20h]=49

00403103   cmp        dword ptr [ebp-20h],0FEh     //与254相比较

0040310A   ja          00403180                                 //跳出switch

0040310C   mov        eax,dword ptr [ebp-20h]        //eax = 49

0040310F   xor         edx,edx                                     // edx清零

00403111   mov         dl,byte ptr  (004031fc)[eax]  // dl = [004031fc+eax]

00403117   jmp         dword ptr [edx*4+4031E8h] // 跳向[edx*4+4031E8h]

case 1:

printf("switch跳转到1!/n");

0040311E   mov        esi,esp

00403120   push        offset string "switch/xcc/xf8/xd7/xaa/xb5/xbd1!/n" (004161e0)

00403125   call         dword ptr [__imp__printf (0041849c)]

0040312B   add         esp,4

0040312E   cmp         esi,esp

00403130   call        _chkesp (00402e5a)

        break;

00403135   jmp         $L94705+17h (00403180)

case 2:

}

00403181,.

    可以看到写法(3)的switch实现方式与写法(2)截然不同。

    请大家注意这样几个地方,sub  exc, 1,执行后ecx = 49,然后与254相比较,大于则跳出switch,否则跳向[edx*4+4031E8h]处,可以看出254是分界点,那么edx里是什么以及[edx*4+4031E8h]是何值呢,看下图004031FC处数据。

    (四 暂略)

     这里并非鼓励将for改为switch,而是应该根据程序运行环境做适当的选择。我们接下来再看一个有趣的例子(1)和(2)哪一个效率要高一些呢?

    for(int i=0; i<200000; i++)            for(int i=0; i<100; i++)

        for(int j=0; j<100; j++)                 for(int j=0; j<200000; j++)

        {                                                   {

            DoSomething();                            DoSomething();

        }                                                   }

    (1)                                                 (2)

    单纯看这个问题和鸡生蛋还是蛋生鸡一样令人不好回答,但实际上(1)要比(2)效率低,虽然二者在反汇编代码中仅循环计数(100,20000互相颠倒)不同。

    一般,我们遵从这样一条经验"在多重嵌套的循环中,如果有可能,应将最长的循环放到最内层,最短的循环放到最外层这样可以减少CPU跨切循环层的次数,从而优化程序的性能"(注1)。类似的问题还有while和for

    while(1)                                                                      for(;;)

    {                                                                                {

        int i = 0;                                                                     int i = 0;

    }                                                                                }

    00402C48  mov ax, 1                                                00402C5A  mov dword ptr[i], 0

    00402C4D  test ax, ax                                              00402C61  jmp  00402C5A

    00402C4F  je     00402C5A  //跳出while

    00402C51  mov dword ptr[i], 0

    00402C58  jmp  00402C48

    答案已经很明显了,while不但指令多,而且占有用寄存器,增加了判断跳转,所以这种情况下for语句要好。类似例子还有,限于篇幅,此处不再一一举例。

    三.  减小尺寸,提高效率

    大家先看这个结构

    typedef  struct_Test

    {

        char  c_1;

        int     v_1;

        char  c_2;

        int     v_2;

        char  c_3;

    }Test, *PTest;

    (1)

    计算一下sizeof(Test)=?,这太简单了,只是加法吗!sizeof(Test)等于11,可惜不是,你的VC编译器会计算出sizeof(Test)=20,不相等的原因是编译器对内存对齐处理造成的,缺省情况下,C/C++编译器默认将结构、栈中的成员数据进行内存对齐。内存对齐的原因是为了提高程序的性能,大部分资料都会提到两点原因,

1.  平台原因:某些硬件平台只能在某些地址处去某些特定类型的数据,否则抛出异常。

2.  性能原因:为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存仅需要做一次访问。

    好了,原因已经找到了,怎么办呢?

    大家一定记得这样一条预编译指令#pragma pack(n),用来改变字节对齐方式,我们只要在程序中加上一句#pragma pack(1)就可以了,这样计算sizeof(Test)就是11了。可这样我们写程序的效率就要降低了,还可能带来平台性的问题,有一个简单的方法可以寻求平衡。

    typedef  struct_Test 1

    {

        char  c_1;

        char  c_2;

        char  c_3;

        int     v_1;

        int     v_2;

    }Test1, *PTest1;

    这样减少了数据尺对又不失效率,sizeof(Test1)等于11。

    当我们自定义的结构过于复杂时很有必要思考一下数据的对齐方式以此来优化程序的效率。

    四.  谁耗费了我的效率

    Class Animal                           class Bird: public Animal

    {                                             {

        public:                                     public:

            Animal();                                 Bird(); 

            ~Animal();                               ~Bird();

        public:                                     public:

            string name, id;                      string Bird_name, Bird_id;

    }                                             }

    有这样一个函数:Bird ReturnBird(Bird b){return b;},如果我们调用这样一个什么对哦偶没有做的函数,会有什么事情发生呢?

    Bird  parrot;

    ReturnBird(parrot);

    整体上看Bird的拷贝构造函数会被调用,用parrot初始化b,接着拷贝构造函数再被调用一次,以b初始化函数的返回值,接着b的析构函数被调用,针对返回值的析构函数也被调用。所以这个什么都没做过的函数耗费的成本是2次拷贝构造函数,2次析构函数。但我们的问题还远没有结束,编译器依然在幕后勤奋着呢,因为Bird含有两个string对象,所以每次构造Bird时,同时也要构造两个string对象,而Bird继承自Animal,所以构造Bird对象的同时必须构造Animal对象,而Animal对象也含有两个string对象,所以构造一个Animal对象也要构造两个string对象。

    好了,我们已经看到,将Bird对象以传值方式调用该函数,会导致一次Bird的拷贝构造,一个Animal的构造函数(注2)及4次string的构造函数。当Bird的副本被销毁时,其析构函数依次被调用,所以一次Bird对象的产生到结束耗费6个构造函数和6个析构函数。

    该例子中Bird对象一共产生两次,一次用于参数,一次用于返回值。所以这个函数的成本是12次构造函数,12次析构函数,这个看似无害的函数在幕后却存在如此多的操作。

    摆脱这样的低效方法是按引用的方式传入参数而非传值方式

    Bird&  ReturnBird(const Bird& b); 这种方式效率会高很多,没有任何对象产生,没有构造析构的调用开销。

    五.  适当声明,注意初始化

    先看不当的声明给我们带来的问题,下面两个函数完成同样功能

    void  Test1(bool bTest)                                 void  Test2(bool bTest)

    {                                                                   {

        Bird  parrot;                                                  if(bTest){

        if(bTest){                                                           Bird parrot;

          ...  // 操作 parrot                                              ...  // 操作  parrot

        }                                                                   }

        return;                                                          return;

    }                                                                   }

    (写法1)                                                          (写法2)

    当Test1,Test2函数被不断调用且bTest = false时,写法2的效率要明显高于写法1。

    对这两个函数各自执行10000000次可知,即便在什么都不做的情况下,两种写法仍然有近似4s的效率之差所以,类对象的声明应该尽量推迟,确定用到时再声明。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值