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

为什么铿锵优化了这个代码中的循环

#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符号位

当input或结果是NaN时,该标准不解释NaN的符号。 但是,请注意,位string上的操作(copy,negate,abs,copySign)指定NaN结果的符号位,有时基于NaN操作数的符号位。 逻辑谓词totalOrder也受NaN操作数的符号位影响。 对于所有其他操作,即使只有一个inputNaN,或者NaN由无效操作产生,该标准也不会指定NaN结果的符号位。

当input和结果都不是NaN时,产品或商的符号是操作数符号的异或; 总和的符号,或者被认为是总和x +( – y)的差异的符号不同于最多的一个加数符号; 并且转换结果的符号,量化操作,roundTo-Integral操作和roundToIntegralExact(见5.3.1)是第一个或唯一操作数的符号。 即使操作数或结果为零或无限时,这些规则也适用。

当两个符号相反的操作数之和(或两个操作数与相同符号的差值)恰好为零时,除了roundTowardNegative之外,所有舍入方向属性的和(或差)符号应为+0; 在该属性下,确切的零和(或差)的符号应为-0。 但是,x + x = x – (-x)即使在x为零时也保留与x相同的符号。

加法的情况

在默认舍入模式 (Round-to-Nearest,Ties-to-Even)下 ,我们看到x+0.0产生xx-0.0-0.0 :在这种情况下,我们有两个符号相反的操作数的总和,是零,§6.3第3段规则这个加法产生+0.0

由于+0.0与原始-0.0不是按位相同的, -0.0是可能作为input发生的合法值,因此编译器必须将代码转换为可能的负零以+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作为input的操作应生成一个NaN,并且inputNaN的有效内容(如果可以在目标格式中表示)。

    这意味着NaN*1.0的指数和尾数(尽pipe不是符号) build议与inputNaN保持不变。 符号没有按照上面的6.3p1规定,但是一个实现可以指定它与源NaN相同。

  • +/- 0.0 ,那么结果是0 ,其符号位与符号位1.0异或,符合§6.3p2。 由于1.0的符号位为0 ,因此输出值与input无关。 因此,即使当x是(负)零时, x*1.0 == x也是如此。

减法案

在默认舍入模式下 ,减法x-0.0也是一个无操作,因为它相当于x + (-0.0) 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 ,那么这就减less到在加法的情况下考虑的加法情况(+0.0) + (-0.0) ,根据§6.3p3规定给+0.0

因为在所有情况下,input值都是合法的,所以可以认为x-0.0是一个no-op,而x == x-0.0是一个重言式。

改变价值的优化

IEEE 754-2008标准有以下有趣的报价:

IEEE 754§10.4文字意义和价值变化优化

[…]

下面的值转换转换,保留了源代码的字面含义:

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

既然所有的NaN和所有的无穷都有相同的指数,并且x+0.0x*1.0对于有限x的正确舍入结果与x+0.0的量值完全相同,它们的指数是相同的。

sNaNs

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

然而,正如user2357112 在注释中指出的那样 ,C11标准明确地留下了未定义的信号NaN( sNaN )的行为,所以允许编译器假定它们不会发生,因此它们引发的exception也不会发生。 C ++ 11标准省略了描述用于发送NaN的行为,因此也使其不确定。

舍入模式

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

为了防止海湾合作委员会采取默认的舍入模式和行为,实验标志-frounding-math可以传递给海湾合作委员会。

结论

即使在-O3 ,Clang和GCC仍然符合IEEE-754标准。 这意味着它必须遵守IEEE-754标准的上述规则。 在这些规则下, x+0.0对于所有的x x 不是完全相同的 ,但是x*1.0 可以这样select :也就是说,当我们

  1. 服从x的build议,当它是一个NaN时,不变地传递x的有效载荷。
  2. NaN结果的符号位由* 1.0保持不变。
  3. x 不是 NaN时,请遵守在商/产品期间XOR符号位的顺序。

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

如果x-0.0x += 0.0不是NOOP。 优化器可以去掉整个循环,因为结果没有被使用。 一般来说,很难说为什么优化器会做出决定。