【问题标题】:Why can't GCC optimize out `std::sqrt`?为什么 GCC 不能优化 `std::sqrt`?
【发布时间】:2016-09-04 05:00:34
【问题描述】:

我有一个简单的程序:

#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

如果我正确理解 as-if 规则,则允许编译器优化不会改变程序可观察行为的代码,包括 I/O 写入等。我丢弃std::sqrt 的结果并且不做任何I/O。此外,我的程序中没有#pragma STDC FENV_ACCESSstd::sqrt 是否有可观察到的副作用,还是 GCC 没有优化调用的其他原因?


(这个问题的初始版本有一个上限10e50,使它成为一个无限循环。同样的事情发生在50,所以nvm关于这个问题。)

【问题讨论】:

  • @Michael std::sqrt 是实现的一部分,所以编译器可以知道它做了什么。
  • sqrt 可能有一个高度优化的汇编器实现,这使得 gcc 很难证明它没有副作用。
  • 它与 -ffast-math 一起使用。没有它,sqrt 可以设置 errno (编辑:等等,那只是 C 平方根吗?e2: ok std::sqrt 也是),所以它可能看不到(或没有尝试看到)它不会。
  • 谁在乎?你不会写这段代码。
  • -fno-math-errno?

标签: c++ gcc optimization


【解决方案1】:

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

在调用 sqrt 之前也取绝对值显然可以确保 g++ 不可能传递一个否定的参数。

【讨论】:

  • IIRC 标准允许设置 errno 但不需要它。
  • @MarcGlisse:如果输入为NaN,则“May”设置errno。如果输入为负数,则设置 errno
  • 那是什么标准?在 C11 中,这取决于 math_errhandling&amp;MATH_ERRNO
  • @MarcGlisse: g++ 7.2.1 20171128 设置 errno 如果您计算负数的 sqrt 并且不指定 -fno-math-errno 并且当然适当地报告 math_errhandling 值为 3。以前请求更简单地要求为所有域错误设置errno。我并不是说这是一个好主意(IMO 是一个糟糕的主意),但这是强制执行的方式。从好的方面来说,生成的额外ucomisd + ja 显然不需要太多时间,因为如果你不计算负数的平方根,它就永远不会花费。然而,在这种情况下执行的代码非常难看......
【解决方案2】:

这有点与循环展开有关。

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

替换为return 0; (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 34678Tavian Barnes' comment)。

【讨论】:

猜你喜欢
  • 2010-09-12
  • 2021-11-22
  • 1970-01-01
  • 2017-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-14
相关资源
最近更新 更多