为什么 Clang 优化了 x * 1.0而不是 x + 0.0?

为什么 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 + + ,因为我想知道答案是否不同。)

6438 次浏览

如果 x-0.0,那么 x += 0.0就不是 NOOP。不过,由于没有使用结果,优化器可以删除整个循环。一般来说,很难说为什么优化器会做出这样的决定。

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

IEEE 7546.3符号位

当输入或结果为 NaN 时,此标准不解释 NaN 的符号。但是,请注意,位字符串上的操作ーー copy、 negate、 abs、 playSign ーー指定 NaN 结果的符号位,有时基于 NaN 操作数的符号位。逻辑谓词 total Order 也受 NaN 操作数的符号位的影响。对于所有其他操作,本标准不指定 NaN 结果的符号位,即使只有一个输入 NaN,或者 NaN 是从无效操作生成的。

当输入和结果都不是 NaN 时,乘积或商的符号是操作数符号的排他 OR; 和的符号或差 x-y 的符号被视为和 x + (- y) ,与最大值不同 附加符号之一; 转换结果的符号、量化运算、 round To-Integraloperation 和 round ToIntegralExact (参见5.3.1)是第一个或唯一一个操作数的符号。即使操作数或结果为零或无限,这些规则也应适用。

当两个有相反符号的操作数的和(或两个有相似符号的操作数的差)正好为零时,除了向负的圆周方向属性外,该和(或差)的符号在所有舍入方向属性中都应为 + 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 7546.2.3 NaN 传播

    将 NaN 操作数传播到其结果并将单个 NaN 作为输入的操作应该生成一个 NaN,其中包含输入 NaN 的有效负载(如果以目标格式表示的话)。

    这意味着 NaN*1.0的指数和尾数(虽然不是符号)是 建议,与输入 NaN不变。符号没有按照上面的6.3 p1指定,但是实现可以指定它与源 NaN相同。

  • +/- 0.0,则结果是 0,其符号位与 1.0的符号位 XORed,符合6.3 p2。由于 1.0的符号位是 0,所以输出值与输入值不变。因此,即使 x为(负)零,x*1.0 == x也是如此。

减法的情况

在默认舍入模式 下,减法 x-0.0也是 no-op,因为它等效于 x + (-0.0)。如果 x

  • NaN,则6.3 p1和6.2.3的应用方式与加法和乘法大致相同。
  • +/- infinity,那么结果就是同一个符号的 +/- infinity
  • 是一个(次)正常数,x-0.0 == x总是。
  • -0.0,那么到6.3 p2我们就得到了 [ ... ]和或差 x-y 的符号被视为和 x + (- y) ,与最多一个加法符号不同;。这迫使我们将 -0.0作为 (-0.0) + (-0.0)的结果来分配,因为 -0.0在符号上不同于附加项的 没有,而 +0.0在符号上不同于附加项的 ,这违反了该子句。
  • +0.0,那么这就减少到上面 加法的情况中考虑的加法情况 (+0.0) + (-0.0),它由6.3 p3判定为 +0.0

因为在所有情况下,输入值作为输出是合法的,所以允许将 x-0.0视为无操作,将 x == x-0.0视为重复。

改变价值的优化

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

IEEE 75410.4字面意义和值变化优化

[...]

以下改变值的转换,除其他外,保留了源代码的字面意义:

  • 当 x 不为零且不是信令 NaN 并且结果与 x 具有相同的指数时,应用标识属性0 + x。
  • 当 x 不是一个信令 NaN 并且结果与 x 具有相同的指数时,应用身份属性1 × x。
  • 更改有效载荷或签名位的安静 NaN。
  • [...]

由于所有 NaN 和所有无穷大都具有相同的指数,并且有限 xx+0.0x*1.0的正确四舍五入结果与 x的大小完全相同,所以它们的指数是相同的。

纳米网络

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

然而,作为 user2357112 评论中指出,C11标准明确地保留了未定义的信令 NaNs (sNaN)行为,所以编译器可以假定它们不会发生,因此它们引发的异常也不会发生。C + + 11标准忽略了对 NaNs 信号传递行为的描述,因此也没有对其进行定义。

舍入模式

在交替舍入模式中,允许的优化可能会发生变化。例如,在 从圆到负无穷大模式下,优化 x+0.0 -> x是允许的,但是禁止 x-0.0 -> x

为了防止 GCC 假设默认舍入模式和行为,可以将实验标志 -frounding-math传递给 GCC。

结论

Clang 和 海湾合作委员会,即使在 -O3,仍然符合 IEEE-754标准。这意味着它必须遵守 IEEE-754标准的上述规则。在这些规则下,所有 xx+0.0不是一模一样的x,但是 x*1.0可能会被选择如此: 也就是说,当我们

  1. x为 NaN 时,遵守通过不改变其有效载荷的建议。
  2. 将 NaN 结果的符号位保留为 * 1.0不变。
  3. x没有 a NaN 时,服从在商/产品期间异或符号位的命令。

要启用 IEEE-754不安全优化 (x+0.0) -> x,需要将标志 -ffast-math传递给 Clang 或 GCC。