问题描述
我一直在研究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
我发现这非常令人惊讶,因为老实说我只是希望清除浮点数的符号位,而这应该只是一条XOR指令。 IEEE-754浮点语义必须要引起这种复杂性,但我只是不明白是什么使它如此变得复杂。为什么只需要比较和有条件的举动?
也许是因为与NaN的比较总是会失败,所以在这种情况下不会清除符号位?但是由于NaN的符号位可以是0或1,所以没关系。
为进行比较,当仅使用std::fabs
时,输出要简单得多,这正是人们所期望的:
abs(float):
andps xmm0,xmmword ptr [rip + .LCPI0_0]
ret
启用-ffast-math
标志时会产生相同的输出。
更新:-O3的gcc 10.2生成:
abs(float):
pxor xmm1,xmm1
comiss xmm1,xmm0
ja .L6
ret
.L6:
xorps xmm0,XMMWORD PTR .LC1[rip]
ret
解决方法
IEEE浮点空间包含许多特殊值,例如正和负0,正和负无穷以及两个“非数字”(NaN)系列。所有这些值都具有定义明确的语义wrt。 <
运算符,因此编译器必须生成正确处理所有特殊情况的代码。
标志-ffast-math
可用于通知编译器它可以假定未使用特殊值,正负0之间的区别无关紧要,并可以进行其他一些简化的假设(例如该添加是关联的)。使用此标志,clang可能会为您的abs
函数生成最佳代码:
abs:
andps .LCPI0_0(%rip),%xmm0
retq
默认情况下是否遵循巴洛克式IEEE语义的选择有些争议。除gcc和clang之外的其他编译器往往会做出相反的选择,它们默认情况下编译快速且紧凑的代码,并且如果需要完全符合IEEE要求,则需要显式命令行标志(例如,对于Intel编译器,-mp
)。