今天在微博上看到有人說 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++ 那個循環中多了一個機器指令造成的。另外通過本文,我們也了解了一些關於匯編的指令優化的知識,希望對大家能有幫助。