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

2023-12-02

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


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

IEEE 754 § 6.3 符号位

当输入或结果为 NaN 时,本标准不解释 NaN 的符号。但请注意,对位字符串的操作(复制、取反、abs、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 is -0.0:在这种情况下,我们有两个符号相反的操作数的和,其和为零,并且 §6.3 第 3 段规则该加法产生+0.0.

Since +0.0 is not bitwise与原件相同-0.0, 然后-0.0是可能作为输入出现的合法值,编译器有义务放入将潜在负零转换为的代码+0.0.

总结:在默认舍入模式下,x+0.0, if x

  • is not -0.0, then x本身就是一个可接受的输出值。
  • is -0.0,则输出值must be +0.0,这与按位不同-0.0.

乘法的例子

默认舍入模式下,不会出现这样的问题x*1.0. If x:

  • 是一个(次)正规数,x*1.0 == x always.
  • is +/- infinity,那么结果是+/- infinity具有相同的符号。
  • is NaN,然后根据

    IEEE 754 § 6.2.3 NaN 传播

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

    这意味着指数和尾数(尽管不是符号)NaN*1.0 are 受到推崇的与输入无关NaN。根据上面的 §6.3p1,该符号未指定,但实现可以指定它与源相同NaN.

  • is +/- 0.0,那么结果是0其符号位与符号位异或1.0,与§6.3p2一致。由于符号位为1.0 is 0,输出值与输入值保持不变。因此,x*1.0 == x即使当x是(负)零。

减法的例子

默认舍入模式下,减法x-0.0也是一个空操作,因为它相当于x + (-0.0). If x is

  • is NaN,则 §6.3p1 和 §6.2.3 的应用方式与加法和乘法大致相同。
  • is +/- infinity,那么结果是+/- infinity具有相同的符号。
  • 是一个(次)正规数,x-0.0 == x always.
  • is -0.0,那么根据 §6.3p2 我们有“[...] 和的符号,或视为和 x + (−y) 的差 x − y 的符号,最多与加数的符号之一不同;“。这迫使我们分配-0.0作为结果(-0.0) + (-0.0), 因为-0.0符号不同于none加数,同时+0.0符号不同于two的加数,违反了本条。
  • is +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 和所有无穷大共享相同的指数,并且正确舍入的结果x+0.0 and x*1.0对于有限的x具有完全相同的大小x,它们的指数是相同的。

sNaNs

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

但是,作为用户2357112在评论中指出,C11 标准明确未定义发信号 NaN 的行为(sNaN),因此编译器可以假设它们不会发生,因此它们引发的异常也不会发生。 C++11 标准省略了对 NaN 信号行为的描述,因此也未定义它。

舍入模式

在替代舍入模式中,允许的优化可能会改变。例如,在舍入到负无穷大模式,优化x+0.0 -> x成为允许的,但是x-0.0 -> x成为禁止的。

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

结论

铿锵和GCC,即使在-O3,仍然符合 IEEE-754 标准。这意味着它必须遵守 IEEE-754 标准的上述规则。x+0.0 is 不相同 to x对全部x根据这些规则,但是x*1.0 可以选择如此: 也就是说,当我们

  1. 遵循建议,以不变的方式传递有效负载x当它是 NaN 时。
  2. 保持 NaN 结果的符号位不变* 1.0.
  3. 遵循商/积期间对符号位进行异或的顺序,当x is not a NaN.

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么 Clang 优化掉 x * 1.0 而不是 x + 0.0? 的相关文章

随机推荐