我在浏览器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
的第一个参数也是答案的目的地。显然xmm 0是我的函数应该放置其返回值的地方,所以如果xmm 0用作第一个参数,就不需要movapd
。但是如果xmm 0是第二个参数,然后它必须通过movapd xmm0, xmm1
将值传递到xmm 0中。(编者注:是的,x86-64 System V在xmm 0、xmm 1等中传递FP参数,并在xmm 0中返回。)
我的问题是:为什么编译器不改变参数本身的顺序,这样movapd
就不必要了?它肯定知道minsd的参数顺序不会改变答案?是不是有什么副作用我不喜欢?
3条答案
按热度按时间t9aqgxwy1#
**
minsd a,b
对于某些特殊的FP值是不可交换的,std::min
**也是不可交换的,除非你使用-ffast-math
。minsd a,b
exactly 实现了(a<b) ? a : b
,包括严格IEEE-754语义中关于符号零和NaN的所有含义。(即它保持源操作数b
为unordered 1或相等)。正如Artyer指出的那样,-0.0
和+0.0
比较相等(即-0. < 0.
为false),但它们是不同的。std::min
是根据(a<b)
比较表达式(cppref)定义的,(a<b) ? a : b
是一种可能的实现,不像std::fmin
,它保证从任何一个操作数传播NaN。有关minss/minsd / maxss/maxsd(以及相应的intrinsic,它们遵循相同的非交换规则,但在某些GCC版本中除外)的更多细节,请参见What is the instruction that gives branchless FP min and max on x86?。
脚注1:记住,
NaN<b
对于任何b
都是假的,对于任何比较 predicate 都是假的。例如,NaN == b
是假的,NaN > b
也是假的。甚至NaN == NaN
也是假的。当一对中的一个或多个是NaN时,它们彼此之间是“无序的”。| 一|B| a < B|
std::min(a,b)
=minsd a,b
|| --|--|--|--|
| 楠|楠|false(无序)|
a
(NaN)|| 楠|非NaN| false(无序)|
a
(NaN)|| 非NaN|楠|false(无序)|
a
(非NaN);fmin
将给予NaN|| 非NaN|非NaN|取决于价值观|
a
或b
,越接近-Inf|| -0.0| +0.0| false(equal)|
a
(-0.0)|| +0.0|-0.0| false(equal)|
a
(+0.0)|请注意,+-Infinity在与有限值的比较中工作良好,所以我不得不写“non-NaN”而不是“finite”。
使用
-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
也会阻止一些即使wrt. exception语义也是100%安全的优化,所以它非常糟糕。在不使用fenv.h
的代码中,你永远不会知道其中的区别。但是将
std::min
视为可交换的只能通过假设没有NaN之类的选项来实现,所以对于那些关心NaN发生了什么的代码来说,绝对不能被称为“安全”。clang -funsafe-math-optimizations -ffinite-math-only
将做你正在寻找的优化。(unsafe-math-optimizations意味着一堆更具体的选项,包括不关心有符号的零语义)。wz1wpwve2#
考虑:
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
不关心NaNui7jx7zq3#
为了扩展现有的说
std::min
不是可交换的答案:这里有一个具体的例子,可以可靠地区分std_min_xy
和std_min_yx
。字符串
distinguish1()
计算为1 / 0.0 > 0.0
,即INFTY > 0.0
或true
。distinguish2()
计算为1 / -0.0 > 0.0
,即-INFTY > 0.0
或false
。(All当然,这是在IEEE规则下。我不认为C++标准 * 强制 * 编译器保留这种特定的行为。老实说,我很惊讶,表达式
-0.0
实际上首先计算为负零!-ffinite-math-only
eliminates this way of telling the difference和-ffinite-math-only -funsafe-math-optimizations
completely eliminates the difference in codegen。