为什么 GCC 不能优化出 'std::sqrt'?

Why can't GCC optimize out `std::sqrt`?

本文关键字:GCC sqrt std 不能 为什么 优化      更新时间:2023-10-16

我有一个简单的程序:

#include <cmath>
int main()
{
    for (int i = 0; i < 50; ++i)
        std::sqrt(i);
}

Clang 3.8在-O3上对其进行了优化,但gcc 6.1没有。它生产以下组件:

## Annotations in comments added after the question was answered,
## for the benefit of future readers.
main:
    pushq   %rbx
    xorl    %ebx, %ebx
    jmp     .L2
.L4:
    pxor    %xmm0, %xmm0         # break cvtsi2sd's false dep on the old value of xmm0
    pxor    %xmm1, %xmm1         # xmm1 = 0.0
    cvtsi2sd        %ebx, %xmm0  # xmm0 = (double)i
    ucomisd %xmm0, %xmm1         # scalar double comparison, setting flags
    ja      .L7                  # if (0.0 > (double)i) sqrt(i);   // The `a` = above.  AT&T syntax reverses the order, but it's jump if  xmm1 above xmm0
.L2:
    addl    $1, %ebx             # i++
    cmpl    $50, %ebx
    jne     .L4                  # i != 50
    xorl    %eax, %eax
    popq    %rbx
    ret                          # return 0
.L7:
    call    sqrt                 # only executed on i < 0.  Otherwise gcc knows std::sqrt has no side effects.
    jmp     .L2

如果我正确理解,就好像规则一样,编译器可以优化出不会改变程序可观察行为的代码,包括I/O写入等。我放弃std::sqrt的结果,不进行任何I/O。此外,我的程序中没有#pragma STDC FENV_ACCESSstd::sqrt是否有可观察到的副作用,或者GCC没有优化调用的另一个原因是什么?


(这个问题的最初版本有一个10e50的上界,使其成为一个无限循环。50也会发生同样的事情,所以请对此发表评论。)

这在某种程度上与循环展开有关。

int main()
{
  for (int i = 0; i <= 16; ++i)  // CHANGED NUMBER OF ITERATIONS
    std::sqrt(i);
}

被替换为CCD_ 7(g++ -O3 -fdump-tree-all)。

如果你看一下.115t.cunroll,你可以看到代码最初被转换成类似于:

// ...
<bb 6>:
i_30 = i_22 + 1;
_32 = (double) i_30;
if (_32 < 0.0)
  goto <bb 7>;
else
  goto <bb 8>;
<bb 7>:
__builtin_sqrt (_32);
<bb 8>:
i_38 = i_30 + 1;
_40 = (double) i_38;
if (_40 < 0.0)
  goto <bb 9>;
else
  goto <bb 10>;
<bb 9>:
__builtin_sqrt (_40);
// ...

编译器用实际数字可以"证明"对sqrt的每次调用都没有副作用(.125t.vrp2):

// ...
<bb 6>:
i_30 = 3;
_32 = 3.0e+0;
if (_32 < 0.0)
  goto <bb 7>;
else
  goto <bb 8>;
<bb 7>:
__builtin_sqrt (_32);
<bb 8>:
i_38 = 4;
_40 = 4.0e+0;
if (_40 < 0.0)
  goto <bb 9>;
else
  goto <bb 10>;
<bb 9>:
__builtin_sqrt (_40);
// ...

如果迭代次数很大,则gcc:

  • 不执行循环展开(除非使用类似--param max-completely-peeled-insns=x--param max-completely-peel-times=y的命令强制执行)
  • 不够"聪明",无法确定调用sqrt(i)没有副作用(但一个小帮助就足够了,例如std::sqrt(std::abs(i))

此外,gcc(v6.x)不支持#pragma STDC FENV_ACCESS

,因此它必须假设此pragma为ON(否则生成的代码可能不正确)(情况更复杂,请参阅bug 34678和Tavian-Barnes的评论)。

原因是标准要求在sqrt传递负数的情况下设置errno。显然,对于值50(对于展开来说太多),g++"忘记"负值是不可能的,而对于较小的值,则展开并通过不断传播发现在每种情况下都可能不需要设置errno

在调用sqrt之前取绝对值显然可以确保g++不可能有一个负参数要传递。