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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM