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

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,删除 memsets,但这不会改变 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 的担忧似乎并不(完全)有效。我已经更新了删除 memsetnew 运算符的问题。【参考方案4】:

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

【讨论】:

我想说这个问题主要不是关于memset,而是关于data2[k] = data1[k] 循环(因为在那之后不再访问data2)。 评论 memset 调用不会改变行为(尽管由于某种原因 6950 毫秒更改为 5500 毫秒)。 同样的论点也适用于operator new。而且实际上没有任何动机将内存分配作为编译器固有的,因此编译器可能不知道操作系统内存分配过程的所有复杂性。 @KonradRudolph:实际上,除了使用内在函数之外,编译器还会作弊。例如,在 LLVM 中,mallocfree(以及 newdelete 的错位名称)是优化器已知的,因此如果它意识到内存未使用(例如在优化之后),那么它也优化分配/解除分配。这当然违反了任何依赖副作用的实现(并且使用LD_LIBRARY_PRELOAD,他们无法知道它),但这是一种使用的策略。 @MatthieuM。他们当然会。我的意思是你不能期望他们作弊。我有点惊讶 LLVM 会打扰到 mallocfree 但没关系。

以上是关于为啥这个无操作循环没有被优化掉?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个 NSMutableArray 没有被填充?

为啥我的 while 循环被激活,但没有其他反应?

优化 pow 算法

为啥电脑总是显示这个,软件程序总是无响应

为啥这个 Swift Playground 打印和循环两次?

Jdk1.6之后对锁的优化