为什么编译器不能优化带0的浮点加法?

Why can the compiler not optimize floating point addition with 0?

我有四个身份函数,它们基本上什么都不做。只有与 1 的乘法可以通过 clang 优化为单个 ret 语句。

float id0(float x) {
    return x + 1 - 1;
}

float id1(float x) {
    return x + 0;
}

float id2(float x) {
    return x * 2 / 2;
}

float id3(float x) {
    return x * 1;
}

下面的编译器输出是:(clang 10, at -O3)

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   3212836864              # float -1
id0(float):                                # @id0(float)
        addss   xmm0, dword ptr [rip + .LCPI0_0]
        addss   xmm0, dword ptr [rip + .LCPI0_1]
        ret
id1(float):                                # @id1(float)
        xorps   xmm1, xmm1
        addss   xmm0, xmm1
        ret
.LCPI2_0:
        .long   1056964608              # float 0.5
id2(float):                                # @id2(float)
        addss   xmm0, xmm0
        mulss   xmm0, dword ptr [rip + .LCPI2_0]
        ret
id3(float):                                # @id3(float)
        ret

我能理解为什么id0id2无法优化。他们增加了可能变成正无穷大的值,第二次操作不会把它改回来。

可是为什么id1优化不了呢?与无穷大相加会产生无穷大,与任何常规数字相加会产生该数字,与 NaN 相加会产生 NaN。那为什么不是像* 1.

这样的"true"恒等运算

Example with Compiler Explorer

IEEE 754 浮点数有两个零值,一负一正。加起来就是正数。

所以 id1(-0.f)0.f,而不是 -0.f
请注意 id1(-0.f) == -0.f 因为 0.f == -0.f.

Demo

另请注意,在 GCC 中使用 -ffast-math 进行编译确实会进行优化并更改结果。

"I have four identity functions which do essentially nothing."

这不是真的。

对于浮点数x + 1 - 1不等于x + 0,等于(x + 1) - 1。所以如果你有例如非常小的 x 那么您将在 x + 1 步骤中丢失那非常小的部分,并且编译器无法知道那是否是您的意图。

而在 x * 2 / 2 的情况下,由于浮点精度,x * 2 也可能不准确,所以你这里有类似的情况,编译器不知道你是否出于某种原因想以这种方式更改 x 的值。

所以它们是相等的:

float id0(float x) {
    return x + (1. - 1.);
}

float id1(float x) {
    return x + 0;
}

这些将是相等的:

float id2(float x) {
    return x * (2. / 2.);
}

float id3(float x) {
    return x * 1;
}

所需的行为肯定可以用另一种方式定义。但是正如 已经提到的,必须使用 -ffast-math

显式激活此优化

Enable fast-math mode. This option lets the compiler make aggressive, potentially-lossy assumptions about floating-point math. These include:

  • Floating-point math obeys regular algebraic rules for real numbers (e.g. + and * are associative, x/y == x * (1/y), and (a + b) * c == a * c + b * c),
  • Operands to floating-point operations are not equal to NaN and Inf, and
  • +0 and -0 are interchangeable.

fast-math 是 clang 和 gcc 的标志集合(这里是 clang 列出的标志):

  • -fno-honor-infinities
  • -fno-honor-nans
  • -fno-math-errno
  • -ffinite-math
  • -fassociative-math
  • -freciprocal-math
  • -fno-signed-zeros
  • -fno-trapping-math
  • -ffp-contract=fast

阅读floating-number-gui.de web page, more about IEEE 754, the C11 standard n1570, the C++11 standard n3337

float id1(float x) {
    return x + 0;
}

如果 x 恰好是信号 NaN,你的 id1 甚至可能不是 return(并且可能 不应该 return).

如果 x 是一个安静的 NaN,那么 id1(x) != x 因为 NaN != NaN(至少 NaN == NaN 应该是 false)。

一些情况下,您需要昂贵的arbitrary precision arithmetic. Then consider using GMPlib

PS。浮点数可以让你做噩梦或获得博士学位,由你选择。他们有时 kill people 或至少造成巨大的金融灾难(例如损失数亿美元或欧元)。