为什么一个朴素的 abs 实现不能在 C++ 中得到很好的优化?
Why can a naive abs implementation not be optimized well in C++?
我正在研究 abs(float)
的简单实现是如何编译的,结果令我非常惊讶:
float abs(float x) {
return x < 0 ? -x : x;
}
在 -O3 处使用 clang 10.1,结果为:
.LCPI0_0:
.long 2147483648 # float -0
.long 2147483648 # float -0
.long 2147483648 # float -0
.long 2147483648 # float -0
abs(float):
movaps xmm2, xmmword ptr [rip + .LCPI0_0]
xorps xmm2, xmm0
xorps xmm3, xmm3
movaps xmm1, xmm0
cmpltss xmm1, xmm3
andps xmm2, xmm1
andnps xmm1, xmm0
orps xmm1, xmm2
movaps xmm0, xmm1
ret
我觉得这很令人惊讶,因为老实说我只是希望 float 的符号位被清除,这应该只是一个 XOR 指令。一定有一些关于 IEEE-754 浮点语义的东西导致了这种复杂化,但我只是不明白是什么让它变得 this 复杂。为什么您只需要比较和有条件的移动?
可能是因为与NaN的比较总是失败,所以在这种情况下符号位没有被清除?但是由于 NaN 的符号位可以是 0 或 1,所以这无关紧要。
相比之下,当简单地使用 std::fabs
时,输出要简单得多,这正是人们所期望的:
abs(float):
andps xmm0, xmmword ptr [rip + .LCPI0_0]
ret
启用 -ffast-math
标志时会产生相同的输出。
更新: gcc 10.2 at -O3 产生:
abs(float):
pxor xmm1, xmm1
comiss xmm1, xmm0
ja .L6
ret
.L6:
xorps xmm0, XMMWORD PTR .LC1[rip]
ret
IEEE 浮点数 space 包含许多特殊值,例如正负 0、正无穷大和负无穷大以及两个“非数字”(NaN) 族。所有这些值都具有 well-defined 语义。 <
运算符,因此编译器必须生成能够正确处理所有特殊情况的代码。
标志 -ffast-math
可用于通知编译器它可能假设未使用特殊值,正负 0 之间的区别无关紧要,并做出一些其他简化假设 (例如那个加法是结合的)。使用此标志,clang 会为您的 abs
函数生成可能是最佳代码:
abs:
andps .LCPI0_0(%rip), %xmm0
retq
选择默认尊重有点古怪的 IEEE 语义是有争议的; gcc 和 clang 以外的编译器倾向于做出相反的选择,它们默认编译快速和紧凑的代码,并且如果需要完全符合 IEEE,则需要明确的 command-line 标志(例如 -mp
在这种情况下英特尔编译器)。
我正在研究 abs(float)
的简单实现是如何编译的,结果令我非常惊讶:
float abs(float x) {
return x < 0 ? -x : x;
}
在 -O3 处使用 clang 10.1,结果为:
.LCPI0_0:
.long 2147483648 # float -0
.long 2147483648 # float -0
.long 2147483648 # float -0
.long 2147483648 # float -0
abs(float):
movaps xmm2, xmmword ptr [rip + .LCPI0_0]
xorps xmm2, xmm0
xorps xmm3, xmm3
movaps xmm1, xmm0
cmpltss xmm1, xmm3
andps xmm2, xmm1
andnps xmm1, xmm0
orps xmm1, xmm2
movaps xmm0, xmm1
ret
我觉得这很令人惊讶,因为老实说我只是希望 float 的符号位被清除,这应该只是一个 XOR 指令。一定有一些关于 IEEE-754 浮点语义的东西导致了这种复杂化,但我只是不明白是什么让它变得 this 复杂。为什么您只需要比较和有条件的移动?
可能是因为与NaN的比较总是失败,所以在这种情况下符号位没有被清除?但是由于 NaN 的符号位可以是 0 或 1,所以这无关紧要。
相比之下,当简单地使用 std::fabs
时,输出要简单得多,这正是人们所期望的:
abs(float):
andps xmm0, xmmword ptr [rip + .LCPI0_0]
ret
启用 -ffast-math
标志时会产生相同的输出。
更新: gcc 10.2 at -O3 产生:
abs(float):
pxor xmm1, xmm1
comiss xmm1, xmm0
ja .L6
ret
.L6:
xorps xmm0, XMMWORD PTR .LC1[rip]
ret
IEEE 浮点数 space 包含许多特殊值,例如正负 0、正无穷大和负无穷大以及两个“非数字”(NaN) 族。所有这些值都具有 well-defined 语义。 <
运算符,因此编译器必须生成能够正确处理所有特殊情况的代码。
标志 -ffast-math
可用于通知编译器它可能假设未使用特殊值,正负 0 之间的区别无关紧要,并做出一些其他简化假设 (例如那个加法是结合的)。使用此标志,clang 会为您的 abs
函数生成可能是最佳代码:
abs:
andps .LCPI0_0(%rip), %xmm0
retq
选择默认尊重有点古怪的 IEEE 语义是有争议的; gcc 和 clang 以外的编译器倾向于做出相反的选择,它们默认编译快速和紧凑的代码,并且如果需要完全符合 IEEE,则需要明确的 command-line 标志(例如 -mp
在这种情况下英特尔编译器)。