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

Meh*_*dad 125 c c++ floating-point optimization clang

为什么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);
}
Run Code Online (Sandbox Code Playgroud)

但不是这段代码中的循环?

#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);
}
Run Code Online (Sandbox Code Playgroud)

(标记为C和C++,因为我想知道每个答案是否不同.)

Iwi*_*ist 163

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

IEEE754§6.3符号位

当输入或结果是NaN时,该标准不解释NaN的符号.但是请注意,对位串的操作 - copy,negate,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相同的符号.

增加的情况

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

由于与原始版本+0.0没有按位相同-0.0,并且这-0.0是一个可能作为输入发生的合法值,编译器必须放入将潜在的负零转换为的代码+0.0.

摘要:在默认的舍入模式下,in x+0.0,ifx

  • 不是 -0.0,那么x它本身就是一个可接受的输出值.
  • -0.0,然后输出值必须是 +0.0,它不是按位相同的-0.0.

乘法的例子

在默认的舍入模式下,不会出现此类问题x*1.0.如果x:

  • 总是是(子)正常数字x*1.0 == x.
  • +/- infinity,那么结果是+/- infinity相同的符号.
  • NaN,然后根据

    IEEE754§6.2.3NaN传播

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

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

  • +/- 0.0,然后结果是0其符号位与符号位异或,1.0与§6.3p2一致.由于符号位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不同,而符号与其中两个加数不同,违反了此条款.
  • +0.0,然后这减少到(+0.0) + (-0.0)上面在"加法案例"中考虑的加法案例, §6.3p3被裁定给予+0.0.

因为对于所有情况,输入值都是合法的输出,所以允许考虑x-0.0无操作和x == x-0.0重言式.

价值变化的优化

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

IEEE754§10.4字面意义和价值改变优化

[...]

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

  • 当x不为零且不是信令NaN时应用identity属性0 + x,结果与x具有相同的指数.
  • 当x不是信令NaN时应用identity属性1×x,结果与x具有相同的指数.
  • 更改安静NaN的有效负载或符号位.
  • [...]

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

sNaNs

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

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

舍入模式

在备用舍入模式中,允许的优化可能会改变.例如,在Round-to-Negative-Infinity模式下,优化x+0.0 -> x变得允许,但是x-0.0 -> x被禁止.

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

结论

Clang和GCC,即使在-O3,仍然符合IEEE-754标准.这意味着它必须遵守IEEE-754标准的上述规则.在这些规则下,对于所有人x+0.0来说都不一致的,但可以选择如此:即,当我们这样做xxx*1.0

  1. 遵守建议,以保持xNaN时的有效载荷不变.
  2. 保持NaN结果的符号位不变 * 1.0.
  3. 服从的商/产品期间以异或符号位,当x为NaN.

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

  • @Mehrdad:附件F,C标准的(可选)部分,指定C遵守IEEE 754,明确不包括信令NaN.(C11 F.2.1.,第一行:"该规范没有定义信令NaN的行为.")声明符合附件F的实现可以通过信令NaN自由地做他们想要的事情.C++标准有自己对IEEE 754的处理,但不管它是什么(我不熟悉),我怀疑它是否也指明了信号NaN的行为. (6认同)
  • 警告:如果它是信号NaN怎么办?(我实际上认为这可能是某种原因,但我真的不知道如何,所以我问.) (2认同)
  • @Mehrdad:sNaN根据标准调用未定义的行为(但它可能由平台很好地定义)所以允许编译器压缩在这里. (2认同)
  • 哦,瞧,这是一个合法适用于C和C ++的问题,通过引用单一标准,对于*两种*语言都可以正确回答。即使当问题涉及语言通用性时,这也会使人们减少抱怨带有C和C ++标记的问题的可能性吗?可悲的是,我认为不是。 (2认同)
  • 当你说x-0时。与 x 相同,值得非常清楚地指定,舍入时这不是真的,因为 0.-0。是-0。在这种情况下(事实上,如果您使用 -frounding-math,gcc 会拒绝简化)。 (2认同)

use*_*ica 35

x += 0.0如果x是的话,不是NOOP -0.0.尽管如此,优化器仍然可以去掉整个循环,因为结果没有被使用.一般来说,很难说为什么优化器会做出决策.

  • 我实际上发布了这个之后我只是*读*为什么`x + = 0.0`不是无操作,但我认为这可能不是原因,因为整个循环应该以任何一种方式进行优化.我可以买它,它不像我希望的那样完全令人信服...... (2认同)
  • @ringø:`long long`是一个整数类型,而不是IEEE754类型. (2认同)