【问题标题】:Why can the compiler not optimize floating point addition with 0? [duplicate]为什么编译器不能用 0 优化浮点加法? [复制]
【发布时间】:2020-09-19 13:52:53
【问题描述】:

我有四个本质上什么都不做的恒等函数。只有与 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这样的“真实”身份操作。

Example with Compiler Explorer

【问题讨论】:

  • 这是一个非常好的问题。我不明白为什么我是唯一的支持者。
  • 不适用于这些具体示例,但请记住,这些结果仅由运算符关联性保证。也就是说,例如,加法运算符 + 和 - 保证从左到右进行评估。因此,隐式转换为浮点数发生在它应该发生的地方,可能是运气(?)。 x + 1 - 11 - 1 + x 具有不同的含义。前者相当于(x + (float)1) - (float)1,后者相当于(float)((int)1 - (int)1) + x;。为避免此类错误,请使用浮点常量1.0f

标签: c++ c optimization compilation compiler-optimization


【解决方案1】:

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

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

Demo

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

【讨论】:

  • 该死,你的速度更快。这里godbolt 用于说明。
  • 写得很好。我开始写答案,但你先写完了,因为你把情况表达得更清楚了。
【解决方案2】:

“我有四个本质上什么都不做的恒等函数。”

这不是真的。

对于浮点数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 可以互换。

fast-math 用于 clang 和 gcc 一组标志(这里是 clang 列出的标志):

  • -fno-honor-infinities
  • -fno-honor-nans
  • -fno-math-errno
  • -有限数学
  • -联想数学
  • -倒数数学
  • -fno-signed-zeros
  • -fno-trapping-math
  • -ffp-contract=fast

【讨论】:

  • x * 2 可能不准确,但不是因为浮点精度,而是因为它的范围:如果x 太大,x * 2 可能会产生无穷大。 x * 2. 不太可能发生这种情况,因为 2. 的类型为 double,而 double 通常具有比 float 更大的范围。
【解决方案3】:

阅读floating-number-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 应该是假的)。

某些情况下,您需要昂贵的arbitrary precision arithmetic。然后考虑使用GMPlib

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

【讨论】:

  • 有趣的链接——尤其是我必须收藏的“杀人”链接。这是另一个相关的:stackoverflow.com/questions/3730019/…
  • 他们有时会杀人或至少造成巨大的金融灾难(例如损失数亿美元或欧元) 这听起来像是个人经历,(ça sent le vécu),你参与了first Ariane 5 lauch失败的调查吗?
  • 如果x 是一个安静的NaN,返回x 仍然会产生id1(x) != x。真正的问题是负零,1 / id1(x) != 1 / x
  • @chrqlie:我在 CEA LIST 工作的实验室确实可以访问 Ariane 5 相关文档,但我个人没有。
猜你喜欢
  • 2012-10-06
  • 2017-12-28
  • 1970-01-01
  • 2012-08-10
  • 2016-05-24
  • 2012-07-01
  • 2011-04-17
  • 2012-03-04
  • 1970-01-01
相关资源
最近更新 更多