zoukankan      html  css  js  c++  java
  • i—比 i++ 快?

    今天在微博上看到有人说 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

  • 相关阅读:
    ParallelsDesktop在windows 10虚拟机重启后分辨率无法保存的问题解决方案
    Windows10 2021年5月功能更新(21H1)的三种方式
    Database "mem:XXX" not found, either pre-create it or allow remote database creation (not recommended in secure environments) [90149-200] 90149/90149 解决方案
    Win7/8下提示OpenSCManager failed 拒绝访问 解决方案
    将 Windows 更新代理更新到最新版本
    解决Eclipse中无法直接使用sun.misc.BASE64Encoder及sun.misc.BASE64Decoder的问题
    【Windows】U 盘装系统,无法格式化所选磁盘分区[错误: 0x8004242d]解决方案
    Boot Camp列表-苹果电脑Windows驱动下载
    selenium4 Timeouts is deprecated
    Selenium4实践1——对比Selenium3,Selenium4更新了什么?
  • 原文地址:https://www.cnblogs.com/eaglet/p/2511174.html
Copyright © 2011-2022 走看看