空循环比C中的非空循环慢
当我试图知道一行C代码用来执行多长时间时,我注意到了一件奇怪的事情:空循环比C中的非空循环慢,c,performance,loops,C,Performance,Loops,当我试图知道一行C代码用来执行多长时间时,我注意到了一件奇怪的事情: int main (char argc, char * argv[]) { time_t begin, end; uint64_t i; double total_time, free_time; int A = 1; int B = 1; begin = clock(); for (i = 0; i<(1<<31)-1; i++); end
int main (char argc, char * argv[]) {
time_t begin, end;
uint64_t i;
double total_time, free_time;
int A = 1;
int B = 1;
begin = clock();
for (i = 0; i<(1<<31)-1; i++);
end = clock();
free_time = (double)(end-begin)/CLOCKS_PER_SEC;
printf("%f\n", free_time);
begin = clock();
for (i = 0; i<(1<<31)-1; i++) {
A += B%2;
}
end = clock();
free_time = (double)(end-begin)/CLOCKS_PER_SEC;
printf("%f\n", free_time);
return(0);
}
为什么空循环比第二个循环使用更多的时间,而第二个循环中有一条指令?当然,我已经尝试了许多变体,但每次,空循环都要比只有一条指令的循环花费更多的时间
请注意,我尝试过交换循环顺序并添加一些预热代码,但这根本没有改变我的问题
我使用代码块作为GNU gcc编译器linux ubuntu 14.04的IDE,并且有一个2.3GHz的四核intel i5(我尝试在单核上运行该程序,这不会改变结果)。假设您的代码使用32位整数
int
类型(您的系统可能会这样做),那么就无法从代码中确定任何内容。相反,它表现出未定义的行为。
foo.c:5:5: error: first parameter of 'main' (argument count) must be of type 'int'
int main (char argc, char * argv[]) {
^
foo.c:13:26: warning: overflow in expression; result is 2147483647 with type 'int' [-Winteger-overflow]
for (i = 0; i<(1<<31)-1; i++);
^
foo.c:19:26: warning: overflow in expression; result is 2147483647 with type 'int' [-Winteger-overflow]
for (i = 0; i<(1<<31)-1; i++) {
^
以下是输出的相关部分(主要功能):
请注意,对clock()
的调用之间不存在任何操作。因此,它们都被编译成完全相同的东西。这个答案假设您已经理解并解决了sharth在未定义行为方面的优点。他还指出了编译器可能在代码上使用的技巧。您应该采取措施确保编译器不会将整个循环识别为无用。例如,将迭代器声明更改为volatile uint64\u t i代码>将阻止删除循环,并且volatile int A
将确保第二个循环实际上比第一个循环做更多的工作。但即使你做到了这一切,你仍然会发现:
程序中后期的代码可能比早期的代码执行得更快
clock()
库函数可能在读取计时器后返回之前导致icache未命中。这将导致在第一个测量间隔内有一些额外的时间。(对于以后的调用,代码已经在缓存中)。但是,这种影响很小,对于clock()
来说肯定太小了,无法测量,即使是一直到磁盘的页面错误。随机上下文切换可以添加到任意时间间隔
更重要的是,您有一个i5cpu,它具有动态时钟功能。当程序开始执行时,时钟频率很可能很低,因为CPU一直处于空闲状态。只要运行该程序,CPU就不再空闲,因此在短时间延迟后,时钟速度将增加。空闲和涡轮增压CPU时钟频率之间的比率可能很大。
(在我的ultrabook的Haswell i5-4200U上,前一个乘数是8,后一个乘数是26,使启动代码的运行速度不到后一个代码的30%!“用于实现延迟的校准”循环在现代计算机上是一个糟糕的主意!)
包括预热阶段(重复运行基准测试,并丢弃第一个结果)以获得更精确的计时,这不仅适用于使用JIT编译器的托管框架我能够在GCC 4.8.2-19ubuntu1中重现这一点,无需优化:
$ ./a.out
4.780179
3.762356
以下是空循环:
0x00000000004005af <+50>: addq $0x1,-0x20(%rbp)
0x00000000004005b4 <+55>: cmpq $0x7fffffff,-0x20(%rbp)
0x00000000004005bc <+63>: jb 0x4005af <main+50>
0x00000000004005af <+50>: nop
0x00000000004005b0 <+51>: addq $0x1,-0x20(%rbp)
0x00000000004005b5 <+56>: cmpq $0x7fffffff,-0x20(%rbp)
0x00000000004005bd <+64>: jb 0x4005af <main+50>
它们现在跑得同样快:
$ ./a.out
3.846031
3.705035
我想这说明了对齐的重要性,但我恐怕无法具体说明:事实上,现代处理器是复杂的。所有执行的指令将以复杂而有趣的方式相互作用。谢谢你
OP和“另一个家伙”显然都发现短循环需要11个周期,而长循环需要9个周期。对于长循环,9个周期是足够的时间,即使有很多操作。对于短循环,由于它太短,一定会有一些暂停,只需添加一个nop
即可使循环足够长,从而避免暂停
如果我们看一下代码,会发生一件事:
0x00000000004005af <+50>: addq $0x1,-0x20(%rbp)
0x00000000004005b4 <+55>: cmpq $0x7fffffff,-0x20(%rbp)
0x00000000004005bc <+63>: jb 0x4005af <main+50>
0x00000000004005af:addq$0x1,-0x20(%rbp)
0x00000000004005b4:cmpq$0x7fffffff,-0x20(%rbp)
0x00000000004005bc:jb 0x4005af
我们读了i
,然后写了回来(addq
)。我们立即再次阅读并比较(cmpq
)。然后我们循环。但是循环使用分支预测。因此,在执行addq
时,处理器并不确定是否允许写入i
(因为分支预测可能是错误的)
然后我们比较i
。处理器将尽量避免从内存中读取i
,因为读取它需要很长时间。相反,一些硬件会记住,我们只是通过添加来写入i
,而不是读取i
,cmpq
指令从存储指令获取数据。不幸的是,我们现在还不确定写入i
是否真的发生了!这样就可以在这里设置一个摊位
这里的问题是,条件跳转、addq
导致条件存储,以及不确定从何处获取数据的cmpq
都非常接近。他们异乎寻常地靠得很近。可能是因为它们靠得太近,处理器此时无法确定是从存储指令中获取i
,还是从内存中读取。然后从内存中读取,这会比较慢,因为它必须等待存储完成。只添加一个nop
就可以给处理器足够的时间
通常你认为有RAM,有缓存。在现代Intel处理器上,读取内存可以读取(从最慢到最快):
内存(RAM)
三级缓存(可选)
二级缓存
一级缓存
尚未写入一级缓存的上一条存储指令李>
因此,处理器在短而慢的循环中的内部功能:
从一级缓存读取i
将1添加到i
Wri
0x00000000004005af <+50>: addq $0x1,-0x20(%rbp)
0x00000000004005b4 <+55>: cmpq $0x7fffffff,-0x20(%rbp)
0x00000000004005bc <+63>: jb 0x4005af <main+50>
0x000000000040061a <+157>: mov -0x24(%rbp),%eax
0x000000000040061d <+160>: cltd
0x000000000040061e <+161>: shr $0x1f,%edx
0x0000000000400621 <+164>: add %edx,%eax
0x0000000000400623 <+166>: and $0x1,%eax
0x0000000000400626 <+169>: sub %edx,%eax
0x0000000000400628 <+171>: add %eax,-0x28(%rbp)
0x000000000040062b <+174>: addq $0x1,-0x20(%rbp)
0x0000000000400630 <+179>: cmpq $0x7fffffff,-0x20(%rbp)
0x0000000000400638 <+187>: jb 0x40061a <main+157>
0x00000000004005af <+50>: nop
0x00000000004005b0 <+51>: addq $0x1,-0x20(%rbp)
0x00000000004005b5 <+56>: cmpq $0x7fffffff,-0x20(%rbp)
0x00000000004005bd <+64>: jb 0x4005af <main+50>
$ ./a.out
3.846031
3.705035
0x00000000004005af <+50>: addq $0x1,-0x20(%rbp)
0x00000000004005b4 <+55>: cmpq $0x7fffffff,-0x20(%rbp)
0x00000000004005bc <+63>: jb 0x4005af <main+50>