为什么GCC不能优化掉 `std::sqrt` 函数?

12

我有一个简单的程序:

#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_ACCESS。那么,std::sqrt是否具有可观察的副作用,或者GCC不优化调用的原因是其他的呢?

4
std::sqrt是实现的一部分,因此编译器可以知道它的作用。 - Baum mit Augen
1
sqrt 可能有高度优化的汇编实现,这使得 gcc 难以证明它没有副作用。 - nwp
16
这段话的意思是:使用“-ffast-math”参数可以避免sqrt函数设置errno值,但如果没有使用此参数,则可能会出现这种情况。作者还提到,他不确定这个问题是否只存在于C语言的平方根函数中,或者std::sqrt函数也有类似问题。 - harold
1
谁关心呢?你不会写这段代码的。 - Lightness Races in Orbit
5
-fno-math-errno 是一个编译器选项,意为禁止在浮点运算中设置errno变量。 - Marc Glisse
显示剩余13条评论
2个回答

12

这与循环展开有些相关。

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,因此必须假定此指示已打开(否则生成的代码可能不正确)(情况更复杂,请参见错误编号34678Tavian Barnes的评论)。


1
此外,gcc(v6.x)不支持#pragma STDC FENV_ACCESS,因此必须假定此pragma已启用。我希望这是真的,但事实并非如此。https://gcc.gnu.org/bugzilla/show_bug.cgi?id=29186 https://llvm.org/bugs/show_bug.cgi?id=8100 - Tavian Barnes

1
原因在于标准要求在传递负数时设置errno。对于值50(过大无法展开),显然g++“忘记”了负值是不可能的,而对于小值,则展开并通过常量传播在每个单独的情况下发现不需要设置errno。在调用sqrt之前取绝对值显然可以确保g++不可能有负参数传递。

如果我没记错的话,标准允许设置errno但不要求。 - Marc Glisse
@MarcGlisse:“May”如果输入为“NaN”,则设置errno。如果输入为负数,则设置errno - 6502
这是什么标准?在C11中,这取决于math_errhandling&MATH_ERRNO - Marc Glisse
@MarcGlisse:如果你计算一个负数的平方根并且没有指定 -fno-math-errno,那么 g++ 7.2.1 20171128 版本会设置 errno 并适当地报告值为 3 的 math_errhandling。之前的要求更简单,即强制对所有域错误设置 errno。我不是说这是个好主意(在我看来是个糟糕的主意),但这是被强制执行的方式。好消息是,显然额外生成的 ucomisd + ja 并不需要花费太多时间,因为如果你不计算负数的平方根,它永远不会被使用。然而,在这种情况下执行的代码相当丑陋... - 6502

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接