将参数std :: min更改为浮点数的编译器输出


74

我在Compiler Explorer中摆弄东西,发现传递给std :: min的参数顺序更改了发出的程序集。

这是Godbolt编译器资源管理器上的示例

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);
}

它将被编译(例如,在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

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

基本指令是minsd。阅读文档时,的第一个参数minsd也是答案的目的地。显然,xmm0是我的函数应该放置其返回值的位置,因此,如果将xmm0用作第一个参数,则无需这样做movapd。但是,如果xmm0是第二个参数,则必须movapd xmm0, xmm1将值放入xmm0中。(编者注:是的,x86-64 System V在xmm0,xmm1等中传递FP args,并在xmm0中返回。)

我的问题是:为什么编译器不切换参数本身的顺序,所以这movapd是没有必要的?当然必须知道,minsd的参数顺序不会改变答案吗?有没有我不喜欢的副作用?


可能仅仅是因为在极少数情况下保存单个寄存器交换是不值得的工作
Alan Birtles

12
@AlanBirtles我希望这不是编写编译器优化的人的想法。好痛 我试图让自己不在乎,因为这对我而言并不重要,但仍然很痛。
RaveTheTadpole

2
@AlanBirtles:Rave是正确的,这不是编译器开发人员的想法。如果这确实是一个错过的优化(而不是严格的FP语义所要求的),那么gcc和clang开发人员可能会喜欢提交一个错过的优化错误。(尽管这可能已经是一个已知的错误;当调用gcc进行寄存器分配时,调用约定要求所设置的硬注册约束有时确实会导致浪费movmovaps指令,在内联后在更大的函数中间看不到。)
彼得·科德斯

2
@bolov我的意思是编译器可以切换minsd操作数的顺序来保存movapd。(但是,据我
所知

为什么将android标签添加到此问题?
彼得·哈达德

Answers:


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和任何比较谓词都是错误的。例如NaN == b是错误的,也是如此NaN > b。偶NaN == NaN是假的。当一对中的一个或多个是NaN时,它们是“无序的”。彼此。


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

对于GCC,请参阅https://gcc.gnu.org/wiki/FloatingPointMath
铛支持类似的选项,包括-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将为您寻找所需的优化。(不安全的数学优化意味着很多更具体的选择,包括不关心带符号的零语义)。


8
-ffast-math忽略的一些细节并不是那么微妙。我很不高兴它优化isnan()falsegodbolt.org/z/zs31Yn
jpa

1
@jpa:是的,“微妙”不是一个很好的描述,除了代码中,NaN在正常情况下是不会发生的,并且您不会尝试处理它。编辑以更具体。
彼得·科德斯

2
请注意,选项的需求movapd是固定的-mavx(假设目标CPU支持AVX),因为AVX添加了指令的非破坏性源(3操作数)编码。
Ruslan

1
@Ruslan:是的。不过,您仍然可以创造出需要额外指令的情况,例如,一个操作数是的内存x = std::min(array[i], x)。甚至AVX也需要循环中的单独负载而不是内存源操作数,因为只有第二个源可以是内存。(而且它不能在最后以水平min进行自动矢量化:std::min对于FP也没有关联)。
Peter Cordes

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


1
-fno-math-errno这里应该无关紧要;std::min是C ++ STL模板函数,而不是C数学库函数,过去对NaN语义(如fmin或)进行错误设置sqrt-fno-math-errno不过,始终是一个好主意,除非在历史代码库中检查errno而不是使用fenv.h
彼得·科德斯

1
应该是,但显然不是,使用clang9.0。 godbolt.org/z/4bdvMa表明,-funsafe-math-optimizations仅凭这一点并不能做到这一点,但是添加-fno-math-errno确实可以将其优化为可交换的。这可能是clang9错误,也可能暗示不安全因素不包括在内的无NaN假设吗?在clang 10.0和GCC中,不安全数+无数错误号仍保留操作数顺序的差异:快速数的其他部分会有所不同。
彼得·科德斯

5

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

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;
}

distinguish1()计算为1 / 0.0 > 0.0,即INFTY > 0.0true
distinguish2()计算为1 / -0.0 > 0.0,即-INFTY > 0.0false
(当然,所有这些都是在IEEE规则下完成的。我不认为C ++标准要求编译器保留这种特殊行为。老实说,令我惊讶的是,表达式-0.0实际上首先被评估为负零!

-ffinite-math-only消除了这种区别的方式,并-ffinite-math-only -funsafe-math-optimizations完全消除了代码的差异

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.