为什么编译器不能优化浮点数加0?

23

我有四个恒等函数,它们基本上什么都不做。只有与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,-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那样是一个“真正的”恒等操作呢。

使用编译器浏览器查看示例


这是一个非常好的问题。我不明白为什么只有我点赞了。 - Bathsheba
1
不适用于这些特定示例,但请记住这些结果仅由运算符结合性保证。例如,加法运算符+和-保证从左到右进行评估。因此,浮点数的隐式转换发生在应该发生的地方,可能是幸运的(?)。x + 1 - 1的含义与1 - 1 + x不同。前者等价于(x + (float)1) - (float)1,后者等价于(float)((int)1 - (int)1) + x;。为避免此类错误,请使用浮点常量1.0f - Lundin
3个回答

20

IEEE 754浮点数有两个零值,一个是负的,一个是正的。当它们相加时,结果为正数。

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

演示

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


你太快了。这里有一个godbolt用于说明。 - n314159
写得很好。我本来想回答,但你表达得更清晰,所以你先完成了。 - Jerry Coffin

8

"我有四个身份函数,它们基本上什么也不做。"

这是不正确的。

对于浮点数,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;
}

所需行为肯定可以以其他方式定义。但正如Nelfeal所述,必须明确使用-ffast-math激活此优化。

启用快速数学模式。此选项允许编译器对浮点数学进行积极、可能有损的假设。这些假设包括:

  • 浮点数学遵循实数的常规代数规则(例如,+和*是可交换的,x/y==x*(1/y),以及(a+b)*c==a*c+b*c)
  • 浮点操作的操作数不等于NaN和Inf,并且
  • +0和-0是互换的。
在clang和gcc中,fast-math是一个标志集合(这里列出了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

1
x * 2 可能不是准确的值,但并非由于浮点精度问题,而是由于其范围限制: 如果 x 太大,x * 2 可能会产生正无穷大。对于 x * 2. 来说,这种情况发生的可能性较小,因为 2. 具有双精度类型 double,而 double 通常比 float 具有更大的范围。 - chqrlie

3

阅读 floating-point-gui.de 网页,了解更多关于 IEEE 754、C11 标准的 n1570,以及 C++11 标准的 n3337

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

如果x是一个信号性的NaN,那么你的id1可能甚至不会返回(也许不应该返回)。
如果x是一个安静的NaN,那么id1(x) != x,因为NaN != NaN(至少NaN == NaN应该是false)。
某些情况下,您需要昂贵的任意精度算法。那么请考虑使用GMPlib
PS.浮点数可以让你噩梦连连或者获得博士学位,由你选择。它们有时会致人死亡,或者至少造成巨大的财务灾难(例如损失数亿美元或欧元)。

有趣的链接 - 尤其是“杀人”链接,我必须把它加入书签。这里还有一个相关的链接:https://dev59.com/h2865IYBdhLWcg3wnP2a - RobertS supports Monica Cellio
它们有时会导致人员伤亡,或者至少造成巨大的财务灾难(例如数亿美元或欧元的损失)。这听起来像是个人经历(ça sent le vécu),你参与了第一次阿丽亚娜5号火箭发射失败的调查吗? - chqrlie
1
如果 x 是一个 quiet NaN,则返回 x 仍会产生 id1(x) != x。真正的问题是负零,在这种情况下,1 / id1(x) != 1 / x - chqrlie
@chrqlie:我在CEA LIST工作的实验室确实可以访问到与Ariane 5相关的文档,但我个人没有。 - Basile Starynkevitch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接