【问题标题】:How to force GCC to assume that a floating-point expression is non-negative?如何强制 GCC 假设浮点表达式是非负的?
【发布时间】:2019-12-31 14:01:55
【问题描述】:

在某些情况下,您知道某个浮点表达式将始终为非负数。例如,在计算向量的长度时,会使用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 cmets中的Postpischil)。

此外,“强制 GCC 假设 x*x 为非负数”应解释为 assert(x*x >= 0.f),因此这也排除了 x*x 为 NaN 的情况。

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

【问题讨论】:

  • x*x 不一定为零或正数。它可能是一个 NaN。不过,我不确定 GCC 在这里处理的是什么。
  • -fno-math-errno 是更安全的选项,它还删除了对sqrtf的调用
  • @EricPostpischil 当然!不过,我仍然想强制编译器认为它不是 NaN。
  • 添加 -ffinite-math-only 告诉 GCC 它可以假设没有无穷大或 NaN。使用它消除了分支和对sqrtf 的调用。由于sqrtf 的无穷大不是错误,这证实了 GCC 在问题中的示例代码中关注的是 NaN。不幸的是,我没有看到一个开关只是说假设没有 NaN,而不是假设没有 NaN 或无穷大,并且在 sqrt 之前插入 if (std::isnan(x)) return x; 不会导致 GCC 识别 x*x 不能是 NaN。
  • @dan04:开关没有说你不能有 NaN;它说编译器可能会假设没有 NaN。因此,您有责任避免 NaN 或承担后果。如果您评估两个无穷大的商,则后续代码可能已在假设未生成 NaN 的情况下进行了优化,因此它可能会走错路,例如。

标签: c++ gcc assembly floating-point micro-optimization


【解决方案1】:

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

#include <cmath>

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

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

但 gcc 不知道如何利用 tmp 是非 NaN 和非负数的承诺。我们仍然得到 (Godbolt) 相同的罐装 asm 序列来检查 x&gt;=0,否则调用 sqrtf 来设置 errno可能会在其他优化通过之后扩展为比较和分支,因此编译器了解更多信息并没有帮助。

这是在启用-fmath-errno 时推测内联sqrt 的逻辑中的一个缺失优化(遗憾的是默认情况下启用)。

你想要的是-fno-math-errno,它是全局安全的

如果您不依赖数学函数来设置 errno,这是 100% 安全的。没有人想要这样,这就是记录掩码 FP 异常的 NaN 传播和/或粘性标志的用途。例如C99/C++11 fenv 通过#pragma STDC FENV_ACCESS ON 访问,然后像fetestexcept() 一样运行。请参阅feclearexcept 中的示例,该示例显示使用它来检测除以零。

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

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

现实世界的 FPU 硬件实际上确实有这些粘性标志,这些标志会一直设置直到被清除,例如x86's mxcsr 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? 解释说,在 ISO C11 中,在数学函数中设置 errno 是可选,并且实现可以表明他们是否这样做。大概在 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

如果您永远不会使用feenableexcept() 揭露任何 FP 异常,您也可以使用-fno-trapping-math。 (尽管此优化不需要该选项,但这里只有 errno-setting 废话是个问题。)。

-fno-trapping-math 不假设没有 NaN 或任何东西,它只假设像 Invalid 或 Inexact 这样的 FP 异常不会真正调用信号处理程序,而不是产生 NaN 或舍入结果。 -ftrapping-math 是默认值,但 it's broken and "never worked" according to GCC dev Marc Glisse。 (即使打开它,GCC 也会进行一些优化,这些优化可以将引发的异常数量从零更改为非零,反之亦然。它会阻止一些安全优化)。但不幸的是,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=54192(默认关闭)仍然处于打开状态。

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

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

【讨论】:

  • assert(x*x &gt;= 0.f) 在发布模式下不会进入预处理代码(定义了NDEBUG)。
  • @Ruslan:我想不出一种方法来表达第一句话既清晰易读又避免暗示assert()总是运行时检查而不是有时什么都没有。 :/我只是要离开它。我想我可以在答案中添加一个脚注,但如果其他人对我掩饰这一点感到困扰,请支持 Ruslan 的评论 :)
  • @StephenG:在 C 中,#pragma STDC FENV_ACCESS ON 支持和关联的fenv stuff(我认为)至少在理论上作为 ISO C 实现的一部分是必需的。我的观点是,由于广泛的硬件(或软 fp)支持,大多数 C++ 实现在实践中也支持它。这些标志不是特殊的或最近的硬件功能,它是主流 FPU 硬件的标准。 (例如,在 x86 上,它是自 8087 以来 x87 的一部分,也是 SSE。)当然也在非 x86 ISA 中。无论如何,这就是为什么有一种 portable ISO C 方式来访问它的原因!
  • @StephenG:更重要的是,我怀疑数学 errno 是否支持在没有 FPU 标志的系统上的 C 或 C++ 实现上,比如某些软件 FP。如果硬件或软件可以检测到 FP 异常,则质量实现将通过fenv 公开该异常。因此,您的观点不是代码在数学函数后检查 errno 或避免 -fno-math-errno 的理由。
  • @StephenG -- 除了一些非常规的环境(细胞矢量处理器、一些早期的 GPGPU 和大多数人永远不会接触的史前事物)之外,IEEE 754 支持(这就是为您提供的粘性标志等)出现
【解决方案2】:

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

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

-fno-math-errno 调用执行的数学函数后不要设置“errno” 使用单个指令,例如“sqrt”。一个依赖于的程序 数学错误处理的 IEEE 异常可能需要使用此标志 在保持 IEEE 算术兼容性的同时提高速度。 任何 -O 选项都不会打开此选项,因为它可能导致 在依赖于精确的程序的错误输出中 实施 IEEE 或 ISO 数学规则/规范 职能。但是,它可能会为执行以下操作的程序生成更快的代码 不需要这些规范的保证。 默认值为 -fmath-errno。 在达尔文系统上,数学库从不设置“errno”。有 因此编译器没有理由考虑这种可能性 它可能,并且 -fno-math-errno 是默认值。

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

【讨论】:

  • 感谢您的努力,但我在问题中特别指出编译器选项(尤其是-fno-math-errno)不是一个选项;我想要针对特定​​案例的临时解决方案。
  • @lisyarus 对不起,我好像错过了这个。我认为您可以使用 __attribute__ 为单个功能设置此选项。这能解决您的问题吗?
  • 这似乎是一件让我很高兴的事情!但是,我不知道如何将no-math-errno 放入函数属性中。
  • @lisyarus 它应该与__attribute__((optimize ("no-math-errno")))#pragma GCC optimize ("no-math-errno") 一起工作,但我无法工作。很奇怪。
  • 也许我也会为此提交一个错误。
【解决方案3】:

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

#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 上被视为 shuffle,因此它甚至不会节省 shuffle)。

【讨论】:

  • 谢谢!不确定我是否可以直接调用 SSE 内在函数(这是编译器的工作,对吗?),但这仍然是一种有趣的方式。
  • @lisyarus 很好,它们存在的意义在于,当编译器无法使用它们时,您可以使用它们,所以这对我来说似乎是一个很好(但可能不寻常)的用例
  • 对,但我希望代码在不支持 SSE 的平台上仍能按预期工作。
  • @lisyarus 那么奔腾2?还是像 ARM 这样的不同 ISA?
  • ARM 可能,是的。但关键是我只想帮助编译器进行优化,而不是完全自己做。
【解决方案4】:

大约一周后,我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

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

【讨论】:

    猜你喜欢
    • 2021-06-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多