以下代码从一个被解释为浮点数的零数组复制到另一个数组,并打印此操作的时间。正如我已经看到许多情况,其中无操作循环只是被编译器(包括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 ++行为(除了更改时间几次)。
似乎编译器可以证明这段代码不会做任何明智的事情,那么为什么不优化循环呢?
答案 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
做了什么。为了实现这一点,必须在头中定义函数(通常不是),或者编译器必须将其视为特殊内在函数。但是除非这些技巧,编译器只是看到一个未知函数的调用,可以有副作用,并为两个调用中的每一个做不同的事情。