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

我好奇的是,当我使用GCC 4.5.1和选项“-O3-lm-funroll-loops-msse4”将pow(a,6)替换为a*a*a*a*a*a时,它使用了5条多指令:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

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

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

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

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


当前回答

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

double foo(double a) {
  return a*a*a*a*a*a;
}

变成

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

使用-O-funcafe数学优化。但是,这种重新排序违反了IEEE-754,因此需要标记。

正如Peter Cordes在一篇评论中指出的,有符号整数可以在没有funsafe数学优化的情况下进行这种优化,因为它恰好在没有溢出的情况下有效,如果有溢出,则会出现未定义的行为。所以你得到

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

只需-O。对于无符号整数,这更容易,因为它们是2的模幂,因此即使在溢出的情况下也可以自由地重新排序。

其他回答

我根本不会期望这种情况得到优化。表达式中包含可以重新组合以删除整个操作的子表达式的情况不太常见。我希望编译器编写者将他们的时间投入到更有可能带来显著改进的领域,而不是涵盖很少遇到的边缘情况。

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

像您在这里所做的那样,向编译器提供提示没有错。重新排列语句和表达式,看看它们会带来什么差异,这是微优化过程中的一个正常和预期的部分。

虽然编译器可能有理由考虑这两个表达式以提供不一致的结果(没有适当的开关),但您无需受到该限制的约束。差异将非常小,以至于如果差异对你很重要,你不应该首先使用标准的浮点运算。

因为浮点数学不是关联的。浮点乘法中操作数的分组方式会影响答案的数值精度。

因此,大多数编译器对重新排序浮点计算非常保守,除非他们能够确定答案不变,或者除非你告诉他们你不在乎数值精度。例如:gcc的-fassociative math选项允许gcc重新关联浮点运算,或者甚至-fast math选项,允许更积极地权衡精度与速度。

这个问题已经有了一些很好的答案,但为了完整起见,我想指出C标准的适用部分是5.1.2.2.3/15(与C++11标准中的1.9/9节相同)。本节指出,只有当运算符真的是结合的或可交换的时,才能重新组合它们。

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

double foo(double a) {
  return a*a*a*a*a*a;
}

变成

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

使用-O-funcafe数学优化。但是,这种重新排序违反了IEEE-754,因此需要标记。

正如Peter Cordes在一篇评论中指出的,有符号整数可以在没有funsafe数学优化的情况下进行这种优化,因为它恰好在没有溢出的情况下有效,如果有溢出,则会出现未定义的行为。所以你得到

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

只需-O。对于无符号整数,这更容易,因为它们是2的模幂,因此即使在溢出的情况下也可以自由地重新排序。

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

FP_CONTRACT pragma的默认状态是实现定义的,因此默认情况下允许编译器进行此类优化。因此,需要严格遵循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,因此遵循上面的段落)。过去,后一种选择并未阻止转型,这意味着GCC在这一点上不符合:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845