如何强制 GCC 假定浮点表达式是非负的?

How to force GCC to assume that a floating-point expression is non-negative?

在某些情况下,您知道某个浮点表达式将始终为非负数。例如,在计算向量的长度时,会 sqrt(a[0]*a[0] + ... + a[N-1]*a[N-1])(注意:我 am 知道 std::hypot,这与问题无关),并且平方根下的表达式显然是非负的。但是,GCC outputs sqrt(x*x) 的以下程序集:

        mulss   xmm0, xmm0
        pxor    xmm1, xmm1
        ucomiss xmm1, xmm0
        ja      .L10
        sqrtss  xmm0, xmm0
        ret
.L10:
        jmp     sqrtf

即比较x*x的结果为零,如果结果为非负,则执行sqrtss指令,否则调用sqrtf

所以,我的问题是:我如何强制 GCC 假设 x*x 总是非负的,以便它跳过比较和 sqrtf 调用,而不编写内联汇编?

我想强调的是,我对本地解决方案感兴趣,而不是做 -ffast-math-fno-math-errno-ffinite-math-only 之类的事情(尽管这些确实解决了问题,感谢评论中的 ks1322、harold 和 Eric Postpischil)。

此外,"force GCC into assuming x*x is non-negative"应该解释为assert(x*x >= 0.f),所以这也排除了x*x为NaN的情况。

我可以接受特定于编译器、特定于平台、CPU 特定等解决方案。

在没有任何全局选项的情况下,这是一种(低开销,但不是免费的)方法来获得没有分支的平方根:

#include <immintrin.h>

float test(float x)
{
    return _mm_cvtss_f32(_mm_sqrt_ss(_mm_set1_ps(x * x)));
}

(在 godbolt 上)

像往常一样,Clang 的洗牌很聪明。 GCC 和 MSVC 在该领域落后,并且无法避免广播。 MSVC 也在做一些神秘的动作..

还有其他方法可以将浮点数转换为 __m128,例如 _mm_set_ss。对于 Clang 来说,这没有什么区别,对于 GCC 来说,这会使代码变得更大更糟(包括 movss reg, reg,它在 Intel 上算作随机播放,因此它甚至不会节省随机播放)。

将选项 -fno-math-errno 传递给 gcc。这解决了问题,而不会使您的代码不可移植或离开 ISO/IEC 9899:2011 (C11) 的领域。

此选项的作用是在数学库函数失败时不尝试设置 errno

       -fno-math-errno
           Do not set "errno" after calling math functions that are executed
           with a single instruction, e.g., "sqrt".  A program that relies on
           IEEE exceptions for math error handling may want to use this flag
           for speed while maintaining IEEE arithmetic compatibility.

           This option is not turned on by any -O option since it can result
           in incorrect output for programs that depend on an exact
           implementation of IEEE or ISO rules/specifications for math
           functions. It may, however, yield faster code for programs that do
           not require the guarantees of these specifications.

           The default is -fmath-errno.

           On Darwin systems, the math library never sets "errno".  There is
           therefore no reason for the compiler to consider the possibility
           that it might, and -fno-math-errno is the default.

鉴于您似乎对数学例程设置不是特别感兴趣errno,这似乎是一个很好的解决方案。

您可以将 assert(x*x >= 0.f) 编写为编译时承诺,而不是 GNU C 中的运行时检查,如下所示:

#include <cmath>

float test1 (float x)
{
    float tmp = x*x;
    if (!(tmp >= 0.0f)) 
        __builtin_unreachable();    
    return std::sqrt(tmp);
}

(相关: 您也可以将 if(!x)__builtin_unreachable() 包装在宏中,然后将其命名为 promise() 或其他名称。)

但是 gcc 不知道如何利用 tmp 是非 NaN 和非负的承诺。我们仍然得到 (Godbolt) 检查 x>=0 并调用 sqrtf 设置 errno 的相同固定 asm 序列。 大概是在其他优化通过之后扩展到比较分支,所以编译器了解更多信息无济于事。

这是在启用 -fmath-errno 时(不幸的是默认情况下启用)推测内联 sqrt 的逻辑中的优化失误。

你想要的是-fno-math-errno,它在全球范围内都是安全的

如果您在设置 errno 时不依赖数学函数,那么这是 100% 安全的。没有人想要那样,这就是记录屏蔽 FP 异常的 NaN 传播 and/or 粘性标志的用途。例如C99/C++11 fenv access via #pragma STDC FENV_ACCESS ON and then functions like fetestexcept(). See the example in feclearexcept 显示使用它来检测被零除。

FP 环境是线程上下文的一部分,而 errno 是全局的。

对这个过时的错误功能的支持不是免费的;除非您有为使用它而编写的旧代码,否则您应该将其关闭。不要在新代码中使用它:使用 fenv。理想情况下,对 -fmath-errno 的支持应尽可能便宜,但很少有人实际使用 __builtin_unreachable() 或其他东西来排除 NaN 输入,这可能使得开发人员不值得花时间来实施优化。不过,如果您愿意,您可以报告优化失败的错误。

真实世界的 FPU 硬件实际上有这些粘性标志,这些标志在清除之前一直保持设置状态,例如x86's mxcsr status/control 为 SSE/AVX 数学或其他 ISA 中的硬件 FPU 注册。在 FPU 可以检测异常的硬件上,高质量的 C++ 实现将支持像 fetestexcept() 这样的东西。如果不是,那么 math-errno 可能也不起作用。

errno for math 是一个陈旧的过时设计,C / C++ 默认仍然坚持使用,现在被广泛认为是一个坏主意。它使编译器更难有效地内联数学函数。或者也许我们并没有像我想的那样坚持下去:Why errno is not set to EDOM even sqrt takes out of domain arguement? 解释说在数学函数中设置 errno 在 ISO C11 中是 optional,一个实现可以表明它们是否这样做它与否。大概也在 C++ 中。

-fno-math-errno-ffast-math-ffinite-math-only 等值更改优化混为一谈是一个很大的错误。 您应该强烈考虑启用它是全局的,或者至少是包含这个函数的整个文件。

float test2 (float x)
{
    return std::sqrt(x*x);
}
# g++ -fno-math-errno -std=gnu++17 -O3
test2(float):   # and test1 is the same
        mulss   xmm0, xmm0
        sqrtss  xmm0, xmm0
        ret

你也可以使用 -fno-trapping-math,如果你永远不会使用 feenableexcept() 来取消屏蔽任何 FP 异常。 (虽然此优化不需要该选项,但这里只有 errno 设置废话才是问题。)。

-fno-trapping-math 不假设没有 NaN 或任何东西,它只假设像 Invalid 或 Inexact 这样的 FP 异常实际上不会调用信号处理程序而不是产生 NaN 或舍入结果。 -ftrapping-math 是默认设置,但 . (Even with it on, GCC does some optimizations which can change the number of exceptions that would be raised from zero to non-zero or vice versa. And it blocks some safe optimizations). But unfortunately, https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192(默认关闭)仍然打开。

如果您确实曾经取消屏蔽异常,那么使用 -ftrapping-math 可能会更好,但是您很少会想要这样做,而不是仅仅在一些数学运算之后检查标志,或者检查南。而且它实际上并没有保留确切的异常语义。

请参阅 以了解 -ftrapping-math 默认值错误地阻止安全优化的情况。 (即使在提升了一个潜在的陷阱操作之后,C 无条件地执行它,gcc 也会生成有条件地执行它的非矢量化 asm!因此,GCC 不仅阻止矢量化,它还改变了异常语义与 C 抽象机。)-fno-trapping-math 启用预期的优化。

大约一周后,我 asked on the matter on GCC Bugzilla 并且他们提供了一个最接近我想法的解决方案

float test (float x)
{
    float y = x*x;
    if (std::isless(y, 0.f))
        __builtin_unreachable();
    return std::sqrt(y);
}

compiles 到以下程序集:

test(float):
    mulss   xmm0, xmm0
    sqrtss  xmm0, xmm0
    ret

不过,我仍然不太确定这里到底发生了什么。