为什么Clang会优化掉x * 1.0,但不会优化掉x + 0.0?

132

为什么Clang会优化掉这段代码中的循环

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

但不包括这段代码中的循环?

#include <time.h>
#include <stdio.h>

static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
    printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(将标记为C和C ++,因为我想知道答案是否对每个语言不同。)


2
当前激活了哪些优化标志? - Iwillnotexist Idonotexist
1
@IwillnotexistIdonotexist:我只是使用了“-O3”,但我不知道如何检查它激活了什么。 - user541686
2
如果在命令行中添加-ffast-math,看看会发生什么将是有趣的。 - plugwash
@Mehrdad 可能是编译器的错误;即使使用默认选项,gcc 也会报错。 - M.M
1
[插入讽刺的评论,指出C不是C ++,即使你已经说过了。] - user253751
显示剩余4条评论
2个回答

174

IEEE 754-2008浮点运算标准和ISO/IEC 10967语言无关算术(LIA)标准,第1部分解释了为什么会这样。

IEEE 754 § 6.3 符号位

当输入或结果为 NaN 时,该标准不解释 NaN 的符号。但是,请注意,对于位串的操作(复制、取反、绝对值、copySign),会指定 NaN 结果的符号位,有时基于 NaN 操作数的符号位。逻辑谓词 totalOrder 也受 NaN 操作数的符号位影响。对于所有其他操作,即使只有一个输入 NaN 或者 NaN 是由无效操作产生的,该标准也不指定 NaN 结果的符号位。

当输入和结果都不是 NaN 时,乘积或商的符号是操作数符号的异或;和或差 x−y 视为和 x+(−y) 的符号与加数的符号至多不同;转换、量化操作、roundTo-Integral 操作和 roundToIntegralExact(参见 5.3.1)的结果的符号是第一个或唯一操作数的符号。即使操作数或结果为零或无穷大,这些规则也适用。

当两个带相反符号的操作数之和(或相似符号的两个操作数之差)恰好为零时,在除 roundTowardNegative 外的所有舍入方向属性中,该和(或差)的符号应为 +0;在该属性下,精确零和(或差)的符号应为 −0。但是,x+x=x−(−x) 即使 x 为零,仍保持与 x 相同的符号。

加法的情况

在默认的舍入模式下 (四舍五入,遇到平局向偶数靠近),我们发现x+0.0产生x,除非x-0.0:在这种情况下,我们有两个相反符号的操作数之和为零,§6.3第3段规定此加法产生+0.0

由于+0.0与原始的-0.0不是按位相同的,并且-0.0是可能作为输入出现的合法值,编译器有义务放入代码,将潜在的负零转换为+0.0

总结:在默认的舍入模式下,在x+0.0中,如果x

  • 不是 -0.0,那么x本身是一个可接受的输出值。
  • -0.0,那么输出值必须是+0.0,这与-0.0不是按位相同的。

乘法情况

在默认舍入模式下,使用x*1.0就不会出现这种问题。如果x

  • 如果是(子)正常数,则x*1.0 == x始终成立。
  • 如果是+/- infinity,则结果为相同符号的+/- infinity
  • 如果是NaN,则根据

    IEEE 754 § 6.2.3 NaN传递

    将NaN操作数传递到其结果并且具有单个NaN作为输入的操作应该产生具有输入NaN有效负载的NaN,如果在目标格式中可表示。

    这意味着NaN*1.0的指数和尾数(虽然不是符号)被“推荐”保持与输入NaN相同。符号根据上面的§6.3p1未指定,但实现可以指定它与源NaN相同。

  • 如果是+/- 0.0,则结果是1.0的符号位异或0的符号位得到的0,与§6.3p2一致。由于1.0的符号位为0,因此输出值与输入值相同。因此,即使x是(负)零,x*1.0 == x也成立。

减法的情况

在默认的舍入模式下,减法 x-0.0 也是一个无操作,因为它等同于 x + (-0.0)。如果 x

  • 如果是NaN,则 §6.3p1 和 §6.2.3 适用于加法和乘法。
  • 如果是+/- infinity,则结果为相同符号的+/- infinity
  • 如果是(次)规范数,则x-0.0 == x总是成立。
  • 如果是-0.0,则根据 §6.3p2 我们有“[...]将差x - y视为和x + (-y)的符号与至多一个加数的符号不同;”。这迫使我们将(-0.0) + (-0.0)的结果赋值为-0.0,因为-0.0的符号与任何一个加数都不同,而+0.0与两个加数的符号不同,违反了此条款。
  • 如果是+0.0,则这归结为上面在加法情况下的案例中考虑过的加法情况(+0.0) + (-0.0),根据§6.3p3被判定为给出+0.0

由于对于所有情况,输入值都合法作为输出值,因此可以将 x-0.0 视为无操作,并将 x == x-0.0 视为重言式。

值更改优化

IEEE 754-2008标准有以下有趣的引用:

IEEE 754 § 10.4 字面意义和值更改优化

[...]

以下是保留源代码字面意义的一些值更改转换:

  • 当 x 不为零且不是信号 NaN 且结果具有与 x 相同的指数时,应用恒等属性 0 + x。
  • 当 x 不是信号 NaN 且结果具有与 x 相同的指数时,应用恒等属性 1 × x。
  • 更改安静 NaN 的有效负载或符号位。
  • [...]

由于所有 NaN 和所有无穷大共享相同的指数,并且对于有限的 xx+0.0x*1.0 的正确舍入结果具有与 x 完全相同的大小,因此它们的指数相同。

sNaNs

信号NaN是浮点陷阱值;它们是特殊的NaN值,如果将其用作浮点操作数,则会导致无效操作异常(SIGFPE)。如果优化掉触发异常的循环,软件将不再具有相同的行为。

然而,正如user2357112在评论中指出的那样,C11标准明确地未定义了信号NaN(sNaN)的行为,因此编译器可以假设它们不会发生,从而也不会引发它们所引发的异常。C++11标准省略了对信号NaN的行为描述,因此也将其留给未定义的状态。

舍入模式

在替代舍入模式下,允许的优化可能会改变。例如,在向负无穷舍入模式下,优化x+0.0 -> x变得可行,但x-0.0 -> x变得被禁止。

为了防止GCC假定默认的舍入模式和行为,可以将实验性标志-frounding-math传递给GCC。
结论
即使在-O3的情况下,Clang和GCC仍然符合IEEE-754标准。这意味着它必须遵守IEEE-754标准的上述规则。x+0.0对于所有x都不是位相同的,但x*1.0可能会被选择为如此:即当我们
1. 遵守建议,在x是NaN时保持有效负载不变。 2. 通过* 1.0保持NaN结果的符号位不变。 3. 在商/积中,当x不是NaN时,遵守异或符号位的顺序。
要启用 IEEE-754 不安全优化 (x+0.0) -> x,需要将标志 -ffast-math 传递给 Clang 或 GCC。

2
警告:如果它是一个信号NaN呢?(我曾经认为这可能是原因,但我不知道具体是怎么回事,所以我问了。) - user541686
6
@Mehrdad:C标准的附录F是关于C遵循IEEE 754的(可选)部分,明确指出不包括信号NaN。 (C11 F.2.1.,第一行:“此规范未定义信号NaN的行为。”)声明符合附录F的实现仍然可以自由处理信号NaN。C++标准有其自己的处理IEEE 754的方式,但无论它是什么(我不熟悉),我都怀疑它也没有指定信号NaN的行为。 - user2357112
2
@Mehrdad:根据标准,sNaN会引发未定义的行为(但在平台上可能是明确定义的),因此编译器在这里进行压缩是允许的。 - Joshua
2
哦,看看,一个问题可以合理地适用于C和C++,并且通过引用单个标准准确地回答了两种语言的问题。这会使人们更不可能抱怨标记为C和C++的问题,即使问题涉及语言共性吗?可悲的是,我认为不会。 - Kyle Strand
2
当你说x-0.与x相同时,值得非常清楚地指出,在四舍五入的情况下,这并不是真实的,因为在这种情况下0.-0.是-0.(事实上,如果你使用-frounding-math,gcc会拒绝简化)。 - Marc Glisse
显示剩余21条评论

38

x += 0.0 如果 x-0.0,那它不是 NOOP。然而,由于结果未被使用,优化器仍然可以剥离整个循环。总而言之,很难说优化器做出决策的原因。


2
我在刚刚阅读完x += 0.0不是一个空操作的原因之后发布了这篇文章,但我认为这可能不是原因,因为整个循环应该被优化掉了。虽然我能理解它,但它并没有像我希望的那样完全令人信服... - user541686
鉴于面向对象语言产生副作用的倾向,我认为确保优化器不改变实际行为将是一件困难的事情。 - Robert Harvey
可能的原因是,由于使用了 long long,因此优化已生效(我使用的是gcc,在double上至少表现相同)。 - Déjà vu
2
@ringø:long long 是一种整数类型,而不是 IEEE754 类型。 - MSalters
1
x -= 0是一样的吗? - Viktor Mellgren
显示剩余2条评论

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