为什么GCC不优化a*a*a*a*a*a到(a*a*a)*(a*a*a)?

xis*_*xis 2083 floating-point assembly gcc compiler-optimization fast-math

我正在对科学应用进行一些数值优化.我注意到的一件事是GCC会pow(a,2)通过编译来优化调用a*a,但调用pow(a,6)没有优化,实际上会调用库函数pow,这会大大降低性能.(相比之下,英特尔C++编译器,可执行文件icc,将消除库调用pow(a,6).)

我很好奇的是,当我更换pow(a,6)a*a*a*a*a*a使用GCC 4.5.1和选项" -O3 -lm -funroll-loops -msse4",它采用5分mulsd的说明:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
Run Code Online (Sandbox Code Playgroud)

如果我写(a*a*a)*(a*a*a),它会产生

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13
Run Code Online (Sandbox Code Playgroud)

这将乘法指令的数量减少到3. icc具有类似的行为.

为什么编译器不能识别这种优化技巧?

Lam*_*eek 2694

因为浮点数学不是关联的.在浮点乘法中对操作数进行分组的方式会影响答案的数值准确性.

因此,大多数编译器对浮点计算的重新排序非常保守,除非他们能够确定答案保持不变,或者除非你告诉他们你不关心数值精度.例如:-fassociative-math选项的gcc允许GCC重新关联浮点运算,或甚至-ffast-math其允许甚至对速度精度的更积极的折衷选择.

  • @xis,'-fassociative-math`不是真的不合适; 只是'a*a*a*a*a*a`和`(a*a*a)*(a*a*a)`是不同的.这与准确性无关; 它是关于标准一致性和严格可重复的结果,例如在任何编译器上都有相同的结果.浮点数已经不准确了.用`-fassociative-math`编译很少是不合适的. (43认同)
  • IIRC C99允许编译器进行这种"不安全"的FP优化,但是GCC(除了x87之外的任何东西)合理地尝试遵循IEEE 754 - 它不是"错误界限";*只有一个正确答案*. (19认同)
  • `pow`的实现细节既不在这里也不在那里; 这个答案甚至没有提到`pow`. (14认同)
  • @nedR:ICC默认允许重新关联.如果要获得符合标准的行为,则需要使用ICC设置`-fp-model precise`.`clang`和`gcc`默认严格一致,重新关联. (13认同)
  • 是.使用-ffast-math,它正在进行这样的优化.好主意!但由于我们的代码比速度更准确,因此最好不要通过它. (9认同)
  • 如果您想要准确性,请选择 (a*a*a)*(a*a*a)。可能的原因是涉及的操作数的大小更平衡,a*a*a*a*a >> a(>> 远大于),以及更少的操作,减少了截断的数量。 (3认同)

Ste*_*non 642

Lambdageek正确地指出,因为关联性不适用于浮点数,所以a*a*a*a*a*ato的"优化"(a*a*a)*(a*a*a)可能会改变该值.这就是为什么C99不允许它(除非用户特别允许,通过编译器标志或编译指示).一般来说,假设程序员为了某个原因编写了她所做的事情,编译器应该尊重这一点.如果你愿意(a*a*a)*(a*a*a),写下来.

但是,写作可能会很痛苦; 为什么编译器只能在你使用时做[你认为是什么]正确的事情pow(a,6)?因为这样做是错误的.在具有良好的数学库的平台,pow(a,6)是显著比任何更准确a*a*a*a*a*a(a*a*a)*(a*a*a).为了提供一些数据,我在Mac Pro上运行了一个小实验,测量了[1,2]之间所有单精度浮点数的^ 6评估中的最差错误:

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07
Run Code Online (Sandbox Code Playgroud)

使用pow而不是乘法树可以将误差限制为4倍.编译器不应(通常不会)进行"优化"以增加错误,除非用户许可(例如通过-ffast-math).

请注意,GCC提供__builtin_powi(x,n)了替代pow( ),它应该生成内联乘法树.如果您想牺牲性能的准确性,但又不想启用快速数学运算,请使用它.

  • 另请注意,Visual C++提供了pow()的"增强"版本.通过使用`flag = 1`调用`_set_SSE2_enable(<flag>)`,如果可能,它将使用SSE2.这会稍微降低精度,但会提高速度(在某些情况下).MSDN:[_ set_SSE2_enable()](http://msdn.microsoft.com/en-us/library/bthd138d(v = vs.71).aspx)和[pow()](http://msdn.microsoft. COM/EN-US /库/ dt5dakze(v = vs.71)的.aspx) (28认同)
  • @TkTech:任何降低的准确性都是由于Microsoft的实现,而不是所用寄存器的大小.如果库编写器如此积极,那么只使用32位寄存器就可以提供*正确舍入的*`pow`.基于SSE的`pow`实现比大多数基于x87的实现更精确*,并且还有一些实现在速度上折衷一些准确性. (17认同)
  • @TkTech:当然,我只想说明准确性的降低是由于图书馆作者做出的选择,而不是SSE使用所固有的. (9认同)
  • @j_random_hacker:因为我正在比较单精度结果,双精度就足以满足黄金标准 - 来自a*a*a*a*a*a的误差是以*为单位计算的*大于*大于任何一个的误差单精度计算. (8认同)
  • 我很想知道你在这里使用什么作为计算相对误差的"黄金标准" - 我通常会预期它会是'a*a*a*a*a*a`,但这显然不是案件!:) (7认同)
  • @DavidHammen Long Double对MSVC的实施没有任何作用,而且不止一个; 他们输入双倍长度加倍.在说它是更好的黄金标准之前,你需要确保长双倍得到很好的支持. (3认同)
  • @Stephen:MSDN文档本身指出,使用SSE2指令时可能会降低准确性(因为它们是默认情况下),因为FPU上的中间寄存器为80位,使用SSE2时为64位. (2认同)
  • *“一般来说,假设程序员写她做的事情是有原因的,编译器应该尊重这一点。如果你想要 (a*a*a)*(a*a*a),那么写。”* On这个推理,自从第一个具有宏功能的汇编器出现以来,一切都可以/应该被遗忘...... (2认同)

san*_*oyd 165

另一个类似的情况下:大多数编译器不会优化a + b + c + d(a + b) + (c + d)(这是一个优化由于第二表达式可以被流水线化更好)和给出(即,作为评价它(((a + b) + c) + d)).这也是因为角落的情况:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));
Run Code Online (Sandbox Code Playgroud)

这输出 1.000000e-05 0.000000e+00

  • 这不完全一样.Changin乘法/除法的顺序(除以除以0)比和/减法的changin顺序更安全.在我看来,编译器应该尝试关联mults./divs.因为这样做会减少操作总数,除了性能增益之外,还可以获得精确的增益. (10认同)
  • @DarioOO:mul/div的风险不同:重新排序要么在最终结果中产生微不足道的变化,要么指数在某个时刻溢出(之前不会有),结果大不相同(可能是+ inf或0). (8认同)
  • 我认为你缺少一些微积分背景.对2个数进行乘法和除法会引入相同的误差量.虽然减去/增加2个数字可能会引入更大的误差,特别是当2个数字的数量级不同时,因此它比sub/add更安全的重新分配mul/divide,因为它引入了最终错误的微小变化. (7认同)
  • @DarioOO:这不安全.乘法和除法与指数的加法和减法相同,并且改变顺序可以容易地使临时数超过指数的可能范围.(不完全相同,因为指数不会损失精度......但是表示仍然非常有限,并且重新排序会导致无法代表的值) (4认同)

Sza*_*lcs 79

Fortran(专为科学计算而设计)具有内置的幂运算符,据我所知,Fortran编译器通常会以与您描述的方式类似的方式优化提升到整数幂.遗憾的是,C/C++没有power运算符,只有库函数pow().这并不妨碍智能编译器pow对特殊情况进行特殊处理并以更快的方式对其进行计算,但似乎它们不太常见......

几年前,我试图以最佳方式计算整数幂更方便,并提出以下建议.它是C++,而不是C,但仍然依赖于编译器在如何优化/内联事物方面有点聪明.无论如何,希望你在实践中发现它有用:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}
Run Code Online (Sandbox Code Playgroud)

对好奇的澄清:这没有找到计算能力的最佳方法,但是因为找到最优解是一个NP完全问题,这对于小功率无论如何都是值得做的(而不是使用pow),没有理由大惊小怪细节.

然后用它作为power<6>(a).

这样可以很容易地输入功率(不需要a用parens 拼出6 秒),并且允许你进行这种优化,而不-ffast-math需要你有精确依赖的东西,例如补偿求和(操作顺序必不可少的例子) .

您可能还会忘记这是C++并且只是在C程序中使用它(如果它与C++编译器一起编译).

希望这可能有用.

编辑:

这是我从编译器得到的:

对于a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
Run Code Online (Sandbox Code Playgroud)

对于(a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0
Run Code Online (Sandbox Code Playgroud)

对于power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
Run Code Online (Sandbox Code Playgroud)

  • 找到最佳功率树可能很难,但由于它只对小功率感兴趣,显而易见的答案是预先计算一次(Knuth提供一个表达100)并使用该硬编码表(这就是gcc内部为powi做的) . (34认同)
  • 在现代处理器上,速度受到延迟的限制.例如,乘法的结果可能在五个周期后可用.在那种情况下,找到创造一些力量的最快方法可能会更棘手. (6认同)
  • 您还可以尝试查找给出相对舍入误差的最低上限或最低平均相对舍入误差的幂树. (3认同)

pic*_*cer 60

当a是整数时,GCC实际上优化a a a a a 到(a a a)(a a a).我试过这个命令:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -
Run Code Online (Sandbox Code Playgroud)

有很多gcc标志,但没有什么花哨的.他们的意思是:从stdin读取; 使用O2优化级别; 输出汇编语言列表而不是二进制; 列表应使用英特尔汇编语言语法; 输入是用C语言编写的(通常是从输入文件扩展名推断语言,但从stdin读取时没有文件扩展名); 并写信给stdout.

这是输出的重要部分.我用一些评论来注释它,表明汇编语言中发生了什么:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp
Run Code Online (Sandbox Code Playgroud)

我在Linux Mint 16 Petra上使用系统GCC,这是一种Ubuntu衍生产品.这是gcc版本:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
Run Code Online (Sandbox Code Playgroud)

正如其他海报所指出的那样,这个选项在浮点时是不可能的,因为浮点运算实际上不是关联的.

  • 这对于整数乘法是合法的,因为二进制补码溢出是未定义的行为.如果会出现溢出,无论重新排序操作如何,它都会发生在某个地方.因此,没有溢出的表达式评估相同,溢出的表达式是未定义的行为,因此编译器可以更改溢出发生的点.gcc也使用`unsigned int`来做到这一点. (11认同)
  • @PeterCordes:我认为它合法的一个更好的原因是,与浮点乘法不同,整数乘法(mod n)是结合的。当然,有符号整数类型溢出仍然是未定义的行为,但假装它不是,你总是会从 `a*a*a*a*a*a` 和 `(a*a* a)*(a*a*a)`。(当然,对于无符号类型,溢出无论如何都不是 UB。) (2认同)

小智 51

因为32位浮点数(例如1.024)不是1.024.在计算机中,1.024是间隔:从(1.024-e)到(1.024 + e),其中"e"表示错误.有些人没有意识到这一点,并且还认为*a中的*表示任意精度数的乘法而没有任何附加到这些数字的错误.有些人没有意识到这一点的原因可能是他们在小学中运用的数学计算:只使用没有错误的理想数字工作,并且相信在执行乘法时简单地忽略"e"是可以的.他们没有看到"浮动a = 1.2","a*a*a"和类似的C代码中隐含的"e".

如果大多数程序员认识到(并且能够执行)C表达式a*a*a*a*a*a实际上并不适用于理想数字的想法,那么GCC编译器将可以自由地优化"a*a*a*a*a*a"to say"t =(a*a); t*t*t",它需要较少的乘法次数.但不幸的是,GCC编译器不知道编写代码的程序员是否认为"a"是带有或不带错误的数字.所以GCC只会做源代码的样子 - 因为这就是GCC用"肉眼"看到的东西.

......一旦你知道什么样的程序员的是,你可以使用"-ffast -数学"开关告诉GCC说:"嘿,GCC,我知道我在做什么!".这将允许GCC将*a*a*a*a*a转换为不同的文本 - 它看起来与a*a*a*a*a*a不同 - 但仍然计算错误间隔内的数字A*A*A*A*A*A.这没关系,因为你已经知道你正在使用间隔,而不是理想的数字.

  • 浮点数是准确的.它们不一定完全符合您的预期.此外,epsilon技术本身就是如何解决现实中的事物的近似,因为真实的预期误差是相对于尾数的比例,即,你通常高达约1 LSB,但是这可能会增加如果你不小心的话,每次操作都要执行,所以在做浮点运算之前先咨询数值分析师.如果可能,请使用合适的库. (51认同)
  • @supercat:IEEE-754非常清楚浮点数据**表示精确值; 第3.2至3.4条是相关章节.当然,您可以选择解释它们,就像您可以选择将"int x = 3"解释为"x"为3 +/- 0.5. (23认同)
  • 对于数值分析,如果您将浮点数解释为不是间隔,而是作为精确值(恰好不是您想要的值),您的大脑会感谢您.例如,如果x在4.5左右,误差小于0.1,并且计算(x + 1) - x,则"间隔"解释会给你一个0.8到1.2的间隔,而"精确值"解释告诉你你的结果将是1,双精度误差最多为2 ^( - 50). (10认同)
  • @supercat:我完全同意,但这并不意味着"距离"并不完全等于其数值; 这意味着数值只是建模的某些物理量的近似值. (7认同)
  • @DonalFellows:IEEE标准要求浮点计算产生的结果最准确地匹配源操作数是精确值时的结果,但这并不意味着它们实际上*表示*精确值.在许多情况下,将0.1f视为(1,677,722 +/- 0.5)/ 16,777,216更有帮助,这应该与该不确定性所暗示的小数位数一起显示,而不是将其视为精确数量(1,677,722 +/- 0.5)/ 16,777,216(应显示为24位小数). (3认同)
  • @StephenCanon:我想这取决于你所说的"代表".在大多数应用程序中,变量用于模拟具体事物.例如,在物理模拟中,它们可以表示各种物体的位置和速度等的X,Y和Z分量.如果我说'Distance = Math.Sqrt((x2-x1)*(x2-x1) +(y2-y1)*(y2-y1)+(z2-z1)*(z2-z1))`,'Distance`的目的是表示(x1,y1,z1)和(x2)之间的欧几里德距离,Y2,Z2).存储在"距离"中的精确数字不太可能是两点之间的精确欧几里德距离,但...... (2认同)

vin*_*c17 32

没有海报提到浮动表达式的收缩(ISO C标准,6.5p8和7.12.2).如果将FP_CONTRACTpragma设置为ON,则允许编译器将表达式a*a*a*a*a*a视为单个操作,就像使用单个舍入精确计算一样.例如,编译器可以用更快和更准确的内部功率函数代替它.这一点特别有趣,因为行为部分由程序员直接在源代码中控制,而最终用户提供的编译器选项有时可能会被错误地使用.

FP_CONTRACTpragma 的默认状态是实现定义的,因此默认情况下允许编译器执行此类优化.因此,需要严格遵循IEEE 754规则的可移植代码应明确地将其设置为OFF.

如果编译器不支持此pragma,则必须保守,避免任何此类优化,以防开发人员选择将其设置为OFF.

GCC不支持这个pragma,但是使用默认选项,它假定它是ON; 因此对于具有硬件FMA的目标,如果想要阻止转换a*b+c为fma(a,b,c),则需要提供一个选项,例如-ffp-contract=off(显式设置pragma OFF)或-std=c99(告诉GCC符合某些C标准版,这里是C99,因此遵循上段).在过去,后一种选择并没有阻止转型,这意味着海湾合作委员会不符合这一点:https://gcc.gnu.org/bugzilla/show_bug.cgi?id = 37845

  • 长期存在的热门问题有时会显示其年龄.这个问题在2011年得到了回答,当时海湾合作委员会可以原因是不尊重当时最近的C99标准.当然现在是2014年,所以GCC ......唉. (3认同)
  • @TimSeguine但是,如果未实现某个编译指示,则其默认值需要对该实现进行最严格的限制。我想这就是大卫在想的。对于GCC,现在此问题已被[如果使用ISO C模式,则已针对FP_CONTRACT进行了修复](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845#c6):它仍然没有实现编译指示,但是在ISO C模式下,现在假定编译指示已关闭。 (2认同)

Mar*_*som 28

我不希望这个案例得到优化.在表达式包含可以重新分组以删除整个操作的子表达式的情况下,通常不会这样.我希望编译器编写者将时间投入到更有可能带来明显改进的领域,而不是覆盖很少遇到的边缘情况.

我很惊讶地从其他答案中得知这个表达式确实可以通过适当的编译器开关进行优化.优化是微不足道的,或者是更常见优化的边缘情况,或者编译器编写者非常彻底.

像你在这里所做的那样,为编译器提供提示没有任何问题.重新排列语句和表达式是微观优化过程中正常和预期的一部分,以了解它们将带来的差异.

虽然编译器可能在考虑两个表达式以提供不一致的结果(没有正确的开关)时是合理的,但是您不需要受该限制的约束.差异将非常微小 - 以至于如果差异对您很重要,那么您首先不应该使用标准浮点运算.

  • 正如另一位评论者所指出的那样,这是荒谬的,这是不正确的; 差异可能是成本的一​​半到10%,如果在一个紧凑的循环中运行,这将转化为许多指令浪费,以获得可能是微不足道的额外精度.说你做蒙特卡罗时不应该使用标准FP就像是说你应该总是用飞机穿越国家; 它忽略了许多外部性.最后,这不是一个不常见的优化; 死代码分析和代码缩减/重构很常见. (17认同)

Bjo*_*orn 28

正如Lambdageek指出浮点乘法不是关联的,你可以得到更低的准确性,但是当获得更好的准确性时,你可以反对优化,因为你想要一个确定性的应用程序.例如,在游戏模拟客户端/服务器中,每个客户端都必须模拟相同的世界,您希望浮点计算是确定性的.

  • @Lanaru整个标准点是语义学; 他的意思显然不明确. (11认同)
  • @Alice看来相当清楚Bjorn在代码意义上使用'deterministic'在不同的平台和不同的编译器版本等上提供相同的结果(外部变量可能超出程序员的控制) - 而不是缺乏运行时的实际数字随机性.如果你指出这不是对这个词的正确使用,我不打算与之争论. (9认同)
  • @Alice你通过争论语义来浪费每个人的时间,包括你自己的时间.他的意思很清楚. (8认同)
  • @greggo除了你对他所说的内容的解释,它仍然是错的; 这就是IEEE 754的全部要点,为跨平台的大多数(如果不是全部)操作提供相同的特性.现在,他没有提到平台或编译器版本,如果你希望每个远程服务器/客户端上的每一个操作都是相同的,这将是一个有效的问题....但是从他的陈述中这并不明显.一个更好的词可能是"可靠相似"或其他东西. (5认同)
  • @greggo不,它仍然是确定性的.在任何意义上都没有添加随机性. (3认同)

Gam*_*per 28

像"pow"这样的库函数通常是精心设计的,以产生最小可能的错误(在通用情况下).这通常是使用样条函数实现近似函数(根据Pascal的注释,最常见的实现似乎是使用Remez算法)

从根本上说是以下操作:

pow(x,y);
Run Code Online (Sandbox Code Playgroud)

具有与任何单个乘法或除法中的误差大致相同幅度的固有误差.

同时进行以下操作:

float a=someValue;
float b=a*a*a*a*a*a;
Run Code Online (Sandbox Code Playgroud)

具有比单个乘法或除法的误差5倍以上的固有误差(因为您组合了5次乘法).

编译器应该非常小心它正在进行的优化:

  1. 如果优化pow(a,6)a*a*a*a*a*a可以提高性能,但显着降低精度浮点数.
  2. 如果优化a*a*a*a*a*apow(a,6)它实际上可能降低准确性,因为"A"是一些特殊的值,该值允许乘法没有错误(2的幂或一些小的整数)
  3. 如果优化pow(a,6)(a*a*a)*(a*a*a)(a*a)*(a*a)*(a*a)仍然可能与pow功能相比失去准确性.

一般来说,你知道对于任意浮点值,"pow"比你最终可以编写的任何函数具有更好的精度,但在某些特殊情况下,多次乘法可能具有更好的准确性和性能,这取决于开发人员选择更合适的函数,最终评论代码,以便其他任何人都不会"优化"该代码.

唯一有意义的事情(个人观点,显然是GCC的选择,不论是任何特定的优化或编译器标志)要优化应该用"a*a"替换"pow(a,2)".这将是编译器供应商应该做的唯一理智的事情.

  • downvoters应该意识到这个答案非常好.我可以引用几十个来源和文档来支持我的答案,而且我可能比任何downvoter更多地参与浮点精度.在StackOverflow中添加其他答案未涵盖的缺失信息是完全合理的,因此要礼貌并解释原因. (7认同)
  • 你的答案的后半部分完全忽略了这一点:编译器应该生成实现源代码所说内容的代码,句号。当你指的是“准确性”时,你也使用“精度”这个词。 (3认同)
  • 在我看来,斯蒂芬·卡农 (Stephen Canon) 的回答涵盖了您要说的内容。您似乎坚持认为 libms 是用样条实现的:它们通常使用参数减少(取决于正在实现的函数)加上单个多项式,其系数已通过或多或少复杂的 Remez 算法变体获得。连接点的平滑度不被认为是 libm 函数值得追求的目标(如果它们最终足够准确,不管域被分成多少个部分,它们都会自动变得非常平滑)。 (2认同)

Ras*_*ban 21

这个问题已经有了一些很好的答案,但为了完整起见,我想指出C标准的适用部分是5.1.2.2.3/15(与第1.9/9节中的相同) C++ 11标准).本节规定,如果运算符实际上是关联的或可交换的,则只能重新分组.


Cha*_*les 12

gcc实际上可以进行这种优化,即使对于浮点数也是如此.例如,

double foo(double a) {
  return a*a*a*a*a*a;
}
Run Code Online (Sandbox Code Playgroud)

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret
Run Code Online (Sandbox Code Playgroud)

-O -funsafe-math-optimizations.但是,这种重新排序违反了IEEE-754,所以它需要标志.

正如Peter Cordes在评论中指出的那样,有符号整数可以进行这种优化,而不会-funsafe-math-optimizations在没有溢出的情况下完全保留,如果存在溢出,则会得到未定义的行为.所以你得到了

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret
Run Code Online (Sandbox Code Playgroud)

只是-O.对于无符号整数,它更容易,因为它们的mod功率为2,因此即使面对溢出也可以自由重新排序.

  • [Godbolt 链接](https://godbolt.org/g/faIFma) 具有 double、int 和 unsigned。gcc 和 clang 都以相同的方式优化所有三个(使用“-ffast-math”) (2认同)