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

我在编译器资源管理器中胡乱操作,发现传递给 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系统 V在 xmm0、 xmm1等中传递 FP 参数,并在 xmm0中返回。)

我的问题是: 为什么编译器不改变参数本身的顺序,这样就不需要 movapd了?它肯定知道,头脑中的论点顺序不会改变答案?是不是有什么副作用,我没注意到?

2865 次浏览

Consider: std::signbit(std::min(+0.0, -0.0)) == false && std::signbit(std::min(-0.0, +0.0)) == true.

The only other difference is if both arguments are (possibly different) NaNs, the second argument should be returned.


You can allow gcc to reorder the arguments by using the -funsafe-math-optimizations -fno-math-errno optimsations (Both enabled by -ffast-math). unsafe-math-optimizations allows the compiler to not care about signed zero, and finite-math-only to not care about NaNs

minsd a,b is not commutative for some special FP values, and neither is std::min, unless you use -ffast-math.

minsd a,b exactly implements (a<b) ? a : b including everything that implies about signed-zero and NaN in strict IEEE-754 semantics. (i.e. it keeps the source operand, b, on unordered1 or equal). As Artyer points out, -0.0 and +0.0 compare equal (i.e. -0. < 0. is false), but they are distinct.

std::min is defined in terms of an (a<b) comparison expression (cppreference), with (a<b) ? a : b as a possible implementation, unlike std::fmin which guarantees NaN propagation from either operand, among other things. (fmin originally came from the C math library, not a C++ template.)

See What is the instruction that gives branchless FP min and max on x86? for much more detail about minss/minsd / maxss/maxsd (and the corresponding intrinsics, which follow the same non-commutative rules except in some GCC versions.)

Footnote 1: Remember that NaN<b is false for any b, and for any comparison predicate. e.g. NaN == b is false, and so is NaN > b. Even NaN == NaN is false. When one or more of a pair are NaN, they are "unordered" wrt. each other.


With -ffast-math (to tell the compiler to assume no NaNs, and other assumptions and approximations), compilers will optimize either function to a single minsd. https://godbolt.org/z/a7oK91

For GCC, see https://gcc.gnu.org/wiki/FloatingPointMath
clang supports similar options, including -ffast-math as a catch-all.

Some of those options should be enabled by almost everyone, except for weird legacy codebases, e.g. -fno-math-errno. (See this Q&A for more about recommended math optimizations). And gcc -fno-trapping-math is a good idea because it doesn't fully work anyway, despite being on by default (some optimizations can still change the number of FP exceptions that would be raised if exceptions were unmasked, including sometimes even from 1 to 0 or 0 to non-zero, IIRC). gcc -ftrapping-math also blocks some optimizations that are 100% safe even wrt. exception semantics, so it's pretty bad. In code that doesn't use fenv.h, you'll never know the difference.

But treating std::min as commutative can only be accomplished with options that assume no NaNs, and stuff like that, so definitely can't be called "safe" for code that cares about exactly what happens with NaNs. e.g. -ffinite-math-only assumes no NaNs (and no infinities)

clang -funsafe-math-optimizations -ffinite-math-only will do the optimization you're looking for. (unsafe-math-optimizations implies a bunch of more specific options, including not caring about signed zero semantics).

To expand on the existing answers that say std::min isn't commutative: Here's a concrete example that reliably distinguishes std_min_xy from std_min_yx. Godbolt:

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() evaluates to 1 / 0.0 > 0.0, i.e. INFTY > 0.0, or true.
distinguish2() evaluates to 1 / -0.0 > 0.0, i.e. -INFTY > 0.0, or false.
(All this under IEEE rules, of course. I don't think the C++ standard mandates that compilers preserve this particular behavior. Honestly I was surprised that the expression -0.0 actually evaluated to a negative zero in the first place!

-ffinite-math-only eliminates this way of telling the difference, and -ffinite-math-only -funsafe-math-optimizations completely eliminates the difference in codegen.