为啥这个无操作循环没有被优化掉?
Posted
技术标签:
【中文标题】为啥这个无操作循环没有被优化掉?【英文标题】:Why is this no-op loop not optimized away?为什么这个无操作循环没有被优化掉? 【发布时间】:2014-02-24 09:31:10 【问题描述】:以下代码从一个解释为浮点数的零数组复制到另一个数组,并打印此操作的时间。正如我所看到的许多无操作循环只是被编译器(包括 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
s,但这不会改变 g++ 的行为(除了多次更改时间)。
似乎编译器可以证明这段代码不会做任何明智的事情,那么为什么不优化循环呢?
【问题讨论】:
从我们的角度来看这不是操作,但是编译器不知道也无法知道 data1 和 data2 的内存块的内容已经相等。 我不能给出明确的答案,但是 a) 任何优化都不是强制性的。 b) 你有两个new
,但没有delete
。
@deviantfan 我依赖于退出时释放的内存,尽管标准似乎无法保证。
即使主要操作系统可以做到这一点,省略删除也是完全错误的。而且,据我所知,这甚至可以解决您的优化问题(“可以”,而不是“将”)。
@pan-:它确实知道,但是不使用 data2 计算的结果。我同意 OP,这种代码通常在我的实践中被优化掉,我必须采取特殊措施确保在编写此类基准时不会发生这种情况。
【参考方案1】:
反正也不是不可能(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++
【讨论】:
【参考方案2】:这个问题中的代码发生了很大变化,导致正确答案无效。这个答案适用于第 5 版:由于代码当前试图读取未初始化的内存,优化器可能会合理地假设发生了意外的事情。
许多优化步骤都有类似的模式:有一种与当前编译状态相匹配的指令模式。如果模式在某个点匹配,则匹配的模式(在参数上)被更有效的版本替换。这种模式的一个非常简单的例子是定义一个随后不使用的变量。在这种情况下,替换只是删除。
这些模式是为正确的代码而设计的。在不正确的代码上,模式可能根本无法匹配,或者它们可能以完全意想不到的方式匹配。第一种情况导致没有优化,第二种情况可能会导致完全不可预知的结果(如果修改后的代码如果进一步优化的话)
【讨论】:
【参考方案3】:您为什么希望编译器对此进行优化?通常很难证明对任意内存地址的写入是“无操作”。在您的情况下,这是可能的,但它需要编译器通过new
跟踪堆内存地址(这又是hard,因为这些地址是在运行时生成的)并且确实没有这样做的动机。
毕竟,您明确地告诉编译器您要分配内存并写入它。可怜的编译器怎么知道你在骗它?
特别是,问题在于堆内存可能与许多其他东西混为一谈。它恰好对您的进程是私有的,但就像我上面所说的那样,证明这对编译器来说是很多工作,与函数本地内存不同。
【讨论】:
没有进一步提及这段记忆,就是这样。 我希望编译器检查数据依赖关系并且只对volatile
内存说“我不知道”。因此,期待一些足够积极的优化似乎很自然。
@VioletGiraffe 证明它。提示:这真的,真的很难。
在这种情况下,非常非常简单。
@KonradRudolph 无论如何,您对 new 的担忧似乎并不(完全)有效。我已经更新了删除 memset
和 new
运算符的问题。【参考方案4】:
编译器可以知道这是一个空操作的唯一方法是它是否知道memset
做了什么。为了实现这一点,函数必须在头文件中定义(通常不是),或者编译器必须将其视为特殊的内在函数。但除了这些技巧,编译器只会看到对未知函数的调用,可能会产生副作用,并且对两个调用中的每一个执行不同的操作。
【讨论】:
我想说这个问题主要不是关于memset
,而是关于data2[k] = data1[k]
循环(因为在那之后不再访问data2
)。
评论 memset
调用不会改变行为(尽管由于某种原因 6950 毫秒更改为 5500 毫秒)。
同样的论点也适用于operator new
。而且实际上没有任何动机将内存分配作为编译器固有的,因此编译器可能不知道操作系统内存分配过程的所有复杂性。
@KonradRudolph:实际上,除了使用内在函数之外,编译器还会作弊。例如,在 LLVM 中,malloc
和 free
(以及 new
和 delete
的错位名称)是优化器已知的,因此如果它意识到内存未使用(例如在优化之后),那么它也优化分配/解除分配。这当然违反了任何依赖副作用的实现(并且使用LD_LIBRARY_PRELOAD
,他们无法知道它),但这是一种使用的策略。
@MatthieuM。他们当然会。我的意思是你不能期望他们作弊。我有点惊讶 LLVM 会打扰到 malloc
和 free
但没关系。以上是关于为啥这个无操作循环没有被优化掉?的主要内容,如果未能解决你的问题,请参考以下文章