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


125

为什么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

static double arr[N]在C中是不允许的;const变量不计入该语言常量表达式
MM

1
[即使您已经将C语言插入C也不是C ++的
刻薄评论中

Answers:


164

IEEE 754-2008浮点算法标准和ISO / IEC 10967语言独立算法(LIA)标准的第1部分回答了为什么会这样。

IEEE 754§6.3符号位

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

当输入或结果都不为NaN时,乘积或商的符号为操作数符号的异或;否则为0。和的符号或被视为和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。

    这意味着的指数和尾数(虽然不是符号)NaN*1.0推荐为从输入不变NaN。根据上面的§6.3p1,未指定符号,但是一种实现可以将其指定为与源相同NaN

  • +/- 0.0,则结果0与符号1.06.3p2一致,其符号位与的符号位XOR在一起。由于的符号位1.00,因此输入的输出值不变。因此,x*1.0 == x即使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在不同的标志2加数,违反本条款。
  • +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信号时,应用标识属性0 + x,并且结果与x具有相同的指数。
  • 当x不是信号NaN且结果具有与x相同的指数时,应用标识属性1×x。
  • 更改安静的NaN的有效载荷或符号位。
  • [...]

由于所有NaN和无穷的所有共享相同的指数,以及正确舍入的结果x+0.0,并x*1.0为有限x具有完全相同的量值相同x,其指数是一样的。

声纳

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

但是,正如user2357112 在注释中指出的那样,C11标准显式地使信号NaNs(sNaN)的行为未定义,因此允许编译器假定它们未发生,因此也不会发生它们引发的异常。C ++ 11标准省略了描述NaN信号传递行为的描述,因此也未定义。

舍入模式

在备用舍入模式下,允许的优化可能会更改。例如,在“取整到负无穷大”模式下,优化x+0.0 -> x成为可能,但x-0.0 -> x被禁止。

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

结论

Clang和GCC甚至在-O3仍保持IEEE-754兼容性。这意味着它必须遵守IEEE-754标准的上述规则。x+0.0没有被比特相同,以x对所有x的那些规则,但x*1.0 可以被选择成这样:即,当我们

  1. 请遵守建议,即x当NaN时不传递有效载荷。
  2. 将NaN结果的正负号保持不变* 1.0
  3. 服从期间的商/产品的符号位的顺序进行XOR,当x为NaN。

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


2
警告:如果它是信号NaN,该怎么办?(我实际上认为这可能是某种原因,但我真的不知道如何,所以我问。)
user541686 2015年

6
@Mehrdad:附录F,它是C标准的(可选)部分,它指定C遵守IEEE 754,但显然没有涵盖信令NaN。(C11 F.2.1,第一行:“此规范未定义信令NaN的行为。”)声明符合附件F的实现仍可以自由地对信令NaN进行所需的操作。C ++标准对IEEE 754有其自己的处理方式,但是无论它是什么(我不熟悉),我都怀疑它是否指定信号NaN行为。
user2357112支持Monica

2
@Mehrdad:sNaN根据标准调用未定义的行为(但是平台可能已很好地定义了该行为),因此允许此处的编译器压缩。
2015年

1
@ user2357112:错误捕获作为其他未使用的计算的副作用的可能性通常会干扰很多优化;如果有时会忽略计算结果,则编译器可能会有用地推迟计算,直到它知道是否将使用该结果为止,但是如果计算会产生重要信号,则可能会很糟糕。
2015年

2
哦,瞧,这是一个适用于C和C ++的问题,可以通过引用一个标准来为两种语言正确回答。即使问题涉及语言通用性,这也会使人们减少抱怨带有C和C ++标记的问题的可能性吗?可悲的是,我认为不是。
凯尔·斯特兰德

35

x += 0.0是不是如果NOOP x-0.0。不过,由于未使用结果,因此优化器仍然可以剥离整个循环。通常,很难说出优化器为什么要做出决定。


2
刚刚阅读了为什么x += 0.0不是空操作之后,实际上将其发布了,但是我认为这可能不是原因,因为整个循环都应该以任何一种方式进行优化。我可以买到它,但并不像我希望的那样完全令人信服...
user541686 2015年

考虑到面向对象语言容易产生副作用,我想很难确定优化程序不会改变实际行为。
罗伯特·哈维

可能是原因,因为long long优化已经生效(使用gcc 进行了优化,gcc的行为至少翻了一番
e2-e4 2015年

2
@ringø:long long是整数类型,不是IEEE754类型。
MSalters 2015年

1
那又如何x -= 0呢?
维克多·梅尔格伦
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.