今天在微博上看到有人说 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++ 那个循环中多了一个机器指令造成的。另外通过本文,我们也了解了一些关于汇编的指令优化的知识,希望对大家能有帮助。