eaglet

本博专注于基于微软技术的搜索相关技术
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

i—比 i++ 快?

Posted on 2012-05-21 09:44  eaglet  阅读(11109)  评论(46编辑  收藏  举报

今天在微博上看到有人说 i—比 i++ 快,我用C写了个程序测试了一下,还真的是快,难道减法运算比加法快?从原理上分析感觉不可能啊,于是深入研究了一下,终于找到原因。

先看一下测试代码:

#include <stdio.h>
#include <time.h>

int main()
{
    int count = 1000000000;

    clock_t cl = clock ();

    for(int i = count; i > 0 ; i--)
    {
    }

    printf("Elapse %u ms\r\n", (clock () - cl));

    cl = clock ();

    for(int i = 0; i < count ; i++)
    {
    }

    printf("Elapse %u ms\r\n", (clock () - cl));

    return 0;
}

 

以上代码在VC 2008 下编译,编译时取消优化选项(如果不取消优化的话,上面两个循环语句由于什么都没干,会被编译器优化掉)。

运行后的结果是
Elapse 2267 ms
Elapse 2569 ms

也就是说减法循环比加法循环10亿次时快300毫秒,超过10%。

从C语言层面上分析,这两个代码几乎是一样的,我一开始也是楞了1分多钟,后来仔细比较两个代码,感觉它们的差别主要在两个地方,一个是加法和减法的差别,一个是for循环的第二个语句中一个是和立即数比较一个是和变量比较。以我掌握的计算机硬件原理知识,我首先排除了第一个差别造成性能影响的可能,那么问题很可能就出在第二个差别上,因为我知道在汇编语言中两个内存变量是不能直接比较的,中间必须要通过寄存器转储一次。这样就会多出至少一个指令。问题可能就在这里。为了验证我的判断,我们来看一下上面代码的汇编语句到底是什么样子的:

 

 

   1:  int main()
   2:  {
   3:  00CC1000  push        ebp  
   4:  00CC1001  mov         ebp,esp 
   5:  00CC1003  sub         esp,10h 
   6:   
   7:      int count = 1000000000;
   8:  00CC1006  mov         dword ptr [count],3B9ACA00h 
   9:   
  10:   
  11:      clock_t cl = clock ();
  12:  00CC100D  call        dword ptr [__imp__clock (0CC209Ch)] 
  13:  00CC1013  mov         dword ptr [cl],eax 
  14:   
  15:      for(int i = count; i > 0 ; i--)
  16:  00CC1016  mov         eax,dword ptr [count] 
  17:  00CC1019  mov         dword ptr [i],eax 
  18:  00CC101C  jmp         main+27h (0CC1027h) 
  19:  00CC101E  mov         ecx,dword ptr [i] //把i的内存值拷贝到寄存器ecx中
  20:  00CC1021  sub         ecx,1 //ecx 减1
  21:  00CC1024  mov         dword ptr [i],ecx //把ecx 的值拷贝到i对应的内存地址,这里完成i--操作 
  22:  00CC1027  cmp         dword ptr [i],0 //i对应的内存值和0进行比较 
  23:  00CC102B  jle         main+2Fh (0CC102Fh) //如果小于等于0,跳转到98行
  24:      {
  25:      }
  26:  00CC102D  jmp         main+1Eh (0CC101Eh)//如果大于0,跳转到19行,继续循环
  27:   
  28:      printf("Elapse %u ms", (clock () - cl));
  29:  00CC102F  call        dword ptr [__imp__clock (0CC209Ch)] 
  30:  00CC1035  sub         eax,dword ptr [cl] 
  31:  00CC1038  push        eax  
  32:  00CC1039  push        offset ___xi_z+30h (0CC20F4h) 
  33:  00CC103E  call        dword ptr [__imp__printf (0CC20A4h)] 
  34:  00CC1044  add         esp,8 
  35:   
  36:      cl = clock ();
  37:  00CC1047  call        dword ptr [__imp__clock (0CC209Ch)] 
  38:  00CC104D  mov         dword ptr [cl],eax 
  39:   
  40:      for(int i = 0; i < count ; i++)
  41:  00CC1050  mov         dword ptr [i],0 
  42:  00CC1057  jmp         main+62h (0CC1062h) 
  43:  00CC1059  mov         edx,dword ptr [i]//把i的内存值拷贝到寄存器edx中 
  44:  00CC105C  add         edx,1 //edx 加 1 
  45:  00CC105F  mov         dword ptr [i],edx //将edx的值拷贝到i变量对应地址 
  46:  00CC1062  mov         eax,dword ptr [i] //将i变量值拷贝到寄存器eax中 
  47:  00CC1065  cmp         eax,dword ptr [count] //用eax 和 count地址上的值进行比较
  48:  00CC1068  jge         main+6Ch (0CC106Ch)//如果大于等于count,跳出循环 
  49:      {
  50:      }
  51:  00CC106A  jmp         main+59h (0CC1059h)//否则跳转到43行继续循环

 

我把汇编语句中的循环部分用红色标记出来,并加上注释。我们可以清楚的看到第二个循环中的汇编指令为7个,第一个为6个,也就是说第一个要比第二个要快 1/7 左右,这个和实际测试出来的结果基本上是吻合的。

那么我们再看看为什么编译器要多一个机器指令。原因是汇编语句不可能对两个内存值直接比较,内存值只能和寄存器进行比较,这个应该是计算机硬件结构决定的,这个问题就导致编译器必须要加一个指令来转储内存值到寄存器中。

再进一步,我们发现编译器似乎很蠢,如果在循环之前把 dword ptr[count] 拷贝到一个寄存器中,比如 ecx ,然后在46 行直接 cmp ecx, dword ptr [i] ,就不需要第47行这个指令了。但事实上编译器可能并没有蠢到这个地步,本文前面说过,我将编译器的优化给禁用了,因为如果优化的话,上面两个for循环将被完全忽略掉,根本不会执行,测试出来的时间为0秒。那么既然我们告诉编译器不优化,编译器也就不会优化这个指令,如果真的按照上面方法优化了,那么在调试环境下,如果我们想在循环中更改 count 的值就比较困难了,需要调试器来做一些编译器要做的事情。

再深入一点,我们还会发现这个汇编语句中还有一个地方可以优化,就是

 

  21:  00CC1024  mov         dword ptr [i],ecx //把ecx 的值拷贝到i对应的内存地址,这里完成i--操作 
  22:  00CC1027  cmp         dword ptr [i],0 //i对应的内存值和0进行比较 

第22行这个地方完全可以优化为 cmp ecx, 0

我们知道对寄存器的读写是最快的,其次是一级缓存,二级缓存,三级缓存,然后才是内存,最后是磁盘。

如果22行优化为 cmp ecx, 0 其运行速度肯定要比 cmp dword ptr[i], 0 要快,因为后面的语句要进行一次寻址,从缓存中读取数据(如果CPU有缓存的话),如果没缓存,就是从内存读一次,那就更慢了。

 

最后我们把i++那个循环改成

for(int i = 0; i < 1000000000 ; i++) 再测一次,结果为

Elapse 2334 ms
Elapse 2290 ms

可以看出两个循环的用时基本上相等了

    for(int i = 0; i < 1000000000 ; i++)
01201050  mov         dword ptr [i],0 
01201057  jmp         main+62h (1201062h) 
01201059  mov         edx,dword ptr [i] 
0120105C  add         edx,1 
0120105F  mov         dword ptr [i],edx 
01201062  cmp         dword ptr [i],3B9ACA00h 
01201069  jge         main+6Dh (120106Dh) 
    {
    }
0120106B  jmp         main+59h (1201059h) 

 

看一下汇编语句,for 循环的第二句改成立即数比较后,汇编语句变成了6个指令了。所以用时也基本相同了。

 

结论:

i++ 和 i-- 性能是没有区别的,之所以我们感觉i--快,是因为在汇编层面上,i++ 那个循环中多了一个机器指令造成的。另外通过本文,我们也了解了一些关于汇编的指令优化的知识,希望对大家能有帮助。

 

微博: http://weibo.com/hubbledotnet