空循环比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

当我试图知道一行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 = 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>