为什么一个朴素的 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 在这种情况下英特尔编译器)。