为什么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
产生x
, x
是-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.0
和x*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 :也就是说,当我们
- 服从
x
的build议,当它是一个NaN时,不变地传递x
的有效载荷。 - NaN结果的符号位由
* 1.0
保持不变。 - 当
x
不是 NaN时,请遵守在商/产品期间XOR符号位的顺序。
要启用IEEE-754不安全优化(x+0.0) -> x
,标志 – -ffast-math
需要传递给Clang或GCC。
如果x
是-0.0
则x += 0.0
不是NOOP。 优化器可以去掉整个循环,因为结果没有被使用。 一般来说,很难说为什么优化器会做出决定。