std::min 的参数顺序更改浮点的编译器输出

Rav*_*ole 75 c++ floating-point x86 assembly android

我在编译器资源管理器中摆弄,我发现传递给 std::min 的参数顺序改变了发出的程序集。

这是 Godbolt Compiler Explorer 上的示例

double std_min_xy(double x, double y) {
    return std::min(x, y);
}

double std_min_yx(double x, double y) {
    return std::min(y, x);
}
Run Code Online (Sandbox Code Playgroud)

这被编译(例如,在 clang 9.0.0 上使用 -O3):

std_min_xy(double, double):                       # @std_min_xy(double, double)
        minsd   xmm1, xmm0
        movapd  xmm0, xmm1
        ret
std_min_yx(double, double):                       # @std_min_yx(double, double)
        minsd   xmm0, xmm1
        ret
Run Code Online (Sandbox Code Playgroud)

如果我将 std::min 更改为老式三元运算符,这种情况仍然存在。它也适用于我尝试过的所有现代编译器(clang、gcc、icc)。

底层指令是minsd. 阅读文档,第一个参数minsd也是答案的目的地。显然 xmm0 是我的函数应该放置其返回值的地方,所以如果 xmm0 用作第一个参数,则movapd不需要。但是如果 xmm0 是第二个参数,那么它必须movapd xmm0, xmm1将值放入 xmm0。(编者注:是的,x86-64 System V在 xmm0、xmm1 等中传递 FP 参数,并在 xmm0 中返回。)

我的问题:为什么编译器不切换参数本身的顺序,这样movapd就没有必要了?它肯定必须知道 minsd 的参数顺序不会改变答案?是否有一些我不欣赏的副作用?

Pet*_*des 78

minsd a,b对于某些特殊的 FP 值不是可交换的,也不是std::min,除非您使用-ffast-math.

minsd a,b 完全实现(a<b) ? a : b包括在严格的 IEEE-754 语义中暗示有关有符号零和 NaN 的所有内容。(即它保持源操作数,b,无序1或等于)。作为Artyer指出,-0.0+0.0比较相等(即-0. < 0.是假的),但它们是不同的。

std::min是根据(a<b)比较表达式 ( cppreference ) 定义的,(a<b) ? a : b作为一种可能的实现,不同于std::fmin它保证从任一操作数传播 NaN,等等。(fmin最初来自 C 数学库,而不是 C++ 模板。)

请参阅在 x86 上提供无分支 FP 最小值和最大值的指令是什么?有关 minss/minsd / maxss/maxsd 的更多详细信息(以及相应的内在函数,它们遵循相同的非交换规则,但在某些 GCC 版本中除外。)

脚注 1:请记住,这NaN<b对于 anyb和任何比较谓词都是错误的。egNaN == b是假的,所以也是NaN > b。甚至NaN == NaN是假的。当一对中的一个或多个是 NaN 时,它们是“无序”的。彼此。


使用-ffast-math(告诉编译器假设没有 NaN 以及其他假设和近似值),编译器会将任一函数优化为单个minsd. https://godbolt.org/z/a7oK91

对于 GCC,请参阅https://gcc.gnu.org/wiki/FloatingPointMath
clang 支持类似的选项,包括-ffast-math作为一个包罗万象的选项。

几乎所有人都应该启用其中的一些选项,除了奇怪的遗留代码库,例如-fno-math-errno. (有关推荐的数学优化的更多信息,请参阅此问答)。并且 gcc-fno-trapping-math是一个好主意,因为它无论如何都不能完全工作,尽管默认情况下是打开的(一些优化仍然可以更改如果未屏蔽异常将引发的 FP 异常的数量,有时甚至从 1 到 0 或 0 到非零,IIRC)。 gcc -ftrapping-math还会阻止一些即使是 100% 安全的优化。异常语义,所以很糟糕。在不使用 的代码中fenv.h,您永远不会知道其中的区别。

但是std::min只能通过假设没有 NaN 之类的选项才能将其视为可交换的,因此对于关心 NaN 究竟会发生什么的代码来说,绝对不能称为“安全”。例如,-ffinite-math-only假设没有 NaN(也没有无穷大)

clang -funsafe-math-optimizations -ffinite-math-only会做你正在寻找的优化。(不安全的数学优化意味着一堆更具体的选项,包括不关心符号零语义)。

  • `-ffast-math` 忽略的一些细节并不那么微妙。我非常惊讶它将 `isnan()` 优化为 `false`:https://godbolt.org/z/zs31Yn (8认同)
  • 请注意,对“movapd”的需求通过“-mavx”选项修复(假设目标 CPU 支持 AVX),因为 AVX 添加了指令的非破坏性源(3 操作数)编码。 (2认同)
  • @Ruslan:确实如此。不过,您仍然可以发明需要额外指令的情况,例如,其中一个操作数是内存,如“x = std::min(array[i], x)”。即使 AVX 也需要在循环中单独加载而不是内存源操作数,因为只有第二个源可以是内存。(并且它无法在末尾自动矢量化水平最小值:“std::min”对于 FP 也不具有关联性)。 (2认同)

Art*_*yer 14

考虑:std::signbit(std::min(+0.0, -0.0)) == false && std::signbit(std::min(-0.0, +0.0)) == true

唯一的另一个区别是,如果两个参数都是(可能不同的)NaN,则应返回第二个参数。


您可以允许 gcc 使用-funsafe-math-optimizations -fno-math-errno优化(均由 启用-ffast-math)重新排序参数。unsafe-math-optimizations允许编译器不关心有符号零,finite-math-only也不关心 NaN


Quu*_*one 5

为了扩大对现有的回答是说std::min是不可交换:这是一个具体的例子是可靠的区别std_min_xystd_min_yx神弩:

bool distinguish1() {
    return 1 / std_min_xy(0.0, -0.0) > 0.0;
}
bool distinguish2() {
    return 1 / std_min_yx(0.0, -0.0) > 0.0;
}
Run Code Online (Sandbox Code Playgroud)

distinguish1()计算为1 / 0.0 > 0.0,即INFTY > 0.0,或true
distinguish2()计算为1 / -0.0 > 0.0,即-INFTY > 0.0,或false
(当然,所有这些都符合 IEEE 规则。我认为 C++ 标准并没有要求编译器保留这种特定的行为。老实说,我很惊讶表达式-0.0实际上首先评估为负零!

-ffinite-math-only消除了这种区分差异的方式,并-ffinite-math-only -funsafe-math-optimizations完全消除了 codegen 中的差异