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



下面的代码从一个解释为浮点数的零数组复制到另一个零数组,并打印此操作的计时。正如我看到的许多情况一样,无操作循环只是被编译器(包括 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 << " msn";
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++ 行为(除了多次更改时序)。

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

无论如何,这并非不可能(clang++ 版本 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++

这个问题中的代码发生了很大的变化,使正确答案无效。这个答案适用于第 5 个版本:当代码当前尝试读取未初始化的内存时,优化器可能会合理地假设正在发生意外的事情。

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

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

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

毕竟,您明确地告诉编译器您要分配内存并写入它。可怜的编译器怎么知道你一直在骗它?

特别是,问题是堆内存可以别名到许多其他东西。它恰好是您的进程私有的,但就像我上面说的,证明这对编译器来说是很多工作,不像函数本地内存。

编译器知道这是无操作的唯一方法是它知道memset做什么。为了实现这一点,函数必须在标头中定义(通常不是),或者编译器必须将其视为特殊内部函数。但是,除了这些技巧之外,编译器只会看到对未知函数的调用,该函数可能会产生副作用,并为两个调用中的每一个执行不同的操作。

最新更新