为什么这个无操作循环没有被优化掉?

时间:2014-02-24 09:31:10

标签: c++ gcc optimization

以下代码从一个被解释为浮点数的零数组复制到另一个数组,并打印此操作的时间。正如我已经看到许多情况,其中无操作循环只是被编译器(包括gcc)优化掉了,我等着改变我的复制数组程序,它会停止进行复制。

#include <iostream>
#include <cstring>
#include <sys/time.h>

static inline long double currentTime()
{
    timespec ts;
    clock_gettime(CLOCK_MONOTONIC,&ts);
    return ts.tv_sec+(long double)(ts.tv_nsec)*1e-9;
}

int main()
{
    size_t W=20000,H=10000;

    float* data1=new float[W*H];
    float* data2=new float[W*H];
    memset(data1,0,W*H*sizeof(float));
    memset(data2,0,W*H*sizeof(float));

    long double time1=currentTime();
    for(int q=0;q<16;++q) // take more time
        for(int k=0;k<W*H;++k)
            data2[k]=data1[k];
    long double time2=currentTime();

    std::cout << (time2-time1)*1e+3 << " ms\n";

    delete[] data1;
    delete[] data2;
}

我用g ++ 4.8.1命令g++ main.cpp -o test -std=c++0x -O3 -lrt编译了这个。这个程序为我打印6952.17 ms。 (我必须设置ulimit -s 2000000才能使其崩溃。)

我还尝试将new的数组创建更改为自动VLA,删除memset,但这不会改变g ++行为(除了更改时间几次)。

似乎编译器可以证明这段代码不会做任何明智的事情,那么为什么不优化循环呢?

4 个答案:

答案 0 :(得分:8)

无论如何,这不是不可能的(clang ++ version 3.3):

clang++ main.cpp -o test -std=c++0x -O3 -lrt

该程序为我打印0.000367毫秒......并查看汇编语言:

...
callq   clock_gettime
movq    56(%rsp), %r14
movq    64(%rsp), %rbx
leaq    56(%rsp), %rsi
movl    $1, %edi
callq   clock_gettime
...

而对于g ++:

...
call    clock_gettime
fildq   32(%rsp)
movl    $16, %eax
fildq   40(%rsp)
fmull   .LC0(%rip)
faddp   %st, %st(1)
.p2align 4,,10
.p2align 3
.L2:
 movl    $1, %ecx
 xorl    %edx, %edx
 jmp     .L5
 .p2align 4,,10
 .p2align 3
 .L3:
 movq    %rcx, %rdx
 movq    %rsi, %rcx
 .L5:
 leaq    1(%rcx), %rsi
 movss   0(%rbp,%rdx,4), %xmm0
 movss   %xmm0, (%rbx,%rdx,4)
 cmpq    $200000001, %rsi
 jne     .L3
 subl    $1, %eax
 jne     .L2
 fstpt   16(%rsp)
 leaq    32(%rsp), %rsi
 movl    $1, %edi
 call    clock_gettime
 ...

编辑(g ++ v4.8.2 / clang ++ v3.3)

源代码 - 原始版本(1)

...
size_t W=20000,H=10000;

float* data1=new float[W*H];
float* data2=new float[W*H];
...

消息来源代码 - 修改后的版本(2)

...
const size_t W=20000;
const size_t H=10000;

float data1[W*H];
float data2[W*H];
...

现在未优化的情况是(1)+ g ++

答案 1 :(得分:3)

此问题中的代码已经发生了很大变化,使得正确的答案无效。此答案适用于第5版:由于代码当前尝试读取未初始化的内存,优化程序可能会合理地假设发生了意外情况。

许多优化步骤都有类似的模式:有一种与当前编译状态相匹配的指令模式。如果模式在某个点匹配,匹配的模式(参数化)将被更高效的版本替换。这种模式的一个非常简单的例子是未随后使用的变量的定义;在这种情况下,替换只是删除。

这些模式是为正确的代码而设计的。在错误的代码上,模式可能无法匹配,或者它们可能以完全无意的方式匹配。第一种情况导致没有优化,第二种情况可能导致完全不可预测的结果(当然如果修改后的代码进一步优化)

答案 2 :(得分:0)

为什么期望编译器对此进行优化?通常很难证明对任意内存地址的写入是“无操作”。在你的情况下,它是可能的,但它需要编译器通过new跟踪堆内存地址(这又是,因为这些地址是在运行时生成的)并且真的没有动力这样做。

毕竟,您告诉编译器显式您要分配内存并写入内存。那个可怜的编译器怎么知道你一直骗到它?

特别是,问题在于堆内存可能会被许多其他东西别名化。它碰巧是你的进程私有的,但正如我上面所说,证明这对编译器来说是很多工作,不像函数本地内存。

答案 3 :(得分:0)

编译器知道这是一个无操作的唯一方法是它是否知道memset做了什么。为了实现这一点,必须在头中定义函数(通常不是),或者编译器必须将其视为特殊内在函数。但是除非这些技巧,编译器只是看到一个未知函数的调用,可以有副作用,并为两个调用中的每一个做不同的事情。