为什么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++是因为我想知道每种情况的答案是否不同.)

推荐答案

IEEE754-2008浮点算术标准和ISO/IEC 10967 Language Independent Arithmetic (LIA) Standard, Part 1回答了为什么会这样.

IEEE 754§6.3符号位

当输入或结果为NaN时,此标准不解释NaN的符号.但是,请注意,对位串的操作-copy、Negate、abs、copy Sign-指定NaN结果的符号位,有时基于NaN操作数的符号位.逻辑谓词totalOrder也受NaN操作数的符号位的影响.对于所有其他操作,即使只有一个输入NAN,或者NAN是从无效操作产生的,本标准也不指定NAN结果的符号位.

当输入和结果都不是NaN时,乘积或商的符号是操作数符号的异或;和或差x的符号− y被认为是x+(−y) ,最多不同于

当符号相反的两个操作数之和(或符号相同的两个操作数之差)正好为零时,除RoundToward负值外,该和(或差)的符号在所有舍入方向属性中应为+0;在该属性下,精确零和(或差)的符号应为−0.然而,x+x=x− (−x) 保留与x相同的符号,即使x为零.

加法的情况

Under the default rounding mode (Round-to-Nearest, Ties-to-Even),我们看到x+0.0产生x,但当x-0.0时除外:在这种情况下,我们有两个带相反符号的操作数之和,它们的和是零,而§6.3第3段规定这个加法产生+0.0.

由于+0.0与原始的-0.0不是bitwise等同的,并且-0.0是可能作为输入出现的合法值,因此编译器必须放入将潜在的负零转换为+0.0的代码.

摘要:在默认舍入模式下,如果为x,则为x+0.0

  • is not -0.0,那么x本身就是一个可接受的输出值.
  • is -0.0,然后是输出值must be +0.0,其按位不等于-0.0.

乘法的情况

Under the default rounding modex*1.0没有这样的问题.如果x:

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

    IEEE 754第6.2.3条

    将一个NaN操作数传播到其结果并将一个NaN作为输入的操作,如果可以用目标格式表示,则应该生成一个带有输入NaN有效负载的NaN.

    这意味着NaN*1.0的指数和尾数(虽然不是符号)是recommended,与输入NaN保持不变.根据上文§6.3p1的规定,该标志未指定,但实施可能会指定其与源NaN相同.

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

减法的例子

Under the default rounding mode,减法x-0.0也是不可操作的,因为它相当于x + (-0.0).如果x

  • NaN,则§6.3p1和§6.2.3的应用方式与加法和乘法基本相同.
  • +/- infinity,那么结果是相同符号的+/- infinity.
  • 是一个(次)正态数字,总是x-0.0 == x.
  • -0.0,那么§6.3p2我们就有了"[...] the sign of a sum, or of a difference x − y regarded as a sum x + (−y), differs from at most one of the addends’ signs;".这迫使我们将-0.0赋值为(-0.0) + (-0.0)的结果,因为-0.0none个加数的符号不同,而+0.0two个加数的符号不同,这违反了本条款.
  • 如果为+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和所有无穷大都具有相同的指数,并且有限x的正确四舍五入结果x+0.0x*1.0x的大小完全相同,因此它们的指数是相同的.

斯南

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

然而,作为用户235711202,C11标准明确地保留了未定义的信号NAN(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.0not bit-identicalx,但x*1.0may be chosen to be so:也就是说,当我们

  1. 服从建议,在为NaN时原封不动地传递x的有效负载.
  2. 将NaN结果的符号位保持* 1.0不变.
  3. xnot个NaN时,在商/积期间服从对符号位进行异或运算的命令.

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

C++相关问答推荐

XV 6中的MLFQ和RR

C/C++中的状态库

malloc实现:判断正确的分配对齐

找出文件是否包含给定的文件签名

使用NameSurname扫描到两个单独的字符串

两个连续的语句是否按顺序排列?

当execvp在C函数中失败时杀死子进程

将fget()与strcMP()一起使用不是正确的比较

Clang:如何强制运行时错误的崩溃/异常由于-fsanitize=undefined

fwrite无法写入满(非常大)缓冲区

初始变量重置后,char[]的赋值将消失

为什么我的Hello World EFI程序构建不正确?

变量的作用域是否在C中的循环未定义行为或实现定义行为的参数中初始化?

C语言中的数字指针

Printf()在C中打印终止字符之后的字符,我该如何解决这个问题?

如何在zOS上编译共享C库

C语言中的指针和多维数组

从系统派生线程调用CRT

与指针的原始C数组或C++向量<;向量<;双>>;

在C中定义函数指针?