我正在对一个科学应用程序进行数值优化。我注意到的一件事是,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也有类似的行为。

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


当前回答

当a为整数时,GCC实际上将a*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 -

有很多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

我在Linux Mint 16 Petra上使用GCC系统,这是一个Ubuntu衍生版本。以下是gcc版本:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

正如其他海报所指出的,在浮点运算中,这个选项是不可能的,因为浮点运算是不相关的。

其他回答

当a为整数时,GCC实际上将a*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 -

有很多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

我在Linux Mint 16 Petra上使用GCC系统,这是一个Ubuntu衍生版本。以下是gcc版本:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

正如其他海报所指出的,在浮点运算中,这个选项是不可能的,因为浮点运算是不相关的。

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

如果大多数程序员认识到(并能够执行)C表达式a*a*a*a*a*a实际上不适用于理想的数字,那么GCC编译器就可以自由地将“a*a*a*a*a*a*a”优化为“t=(a*a);t*t*t”,这需要更少的乘法运算。但不幸的是,GCC编译器不知道编写代码的程序员是否认为“a”是一个有或没有错误的数字。所以GCC只会做源代码的样子——因为这是GCC用“肉眼”看到的。

…一旦你知道自己是什么样的程序员,你就可以使用“-fast math”开关告诉GCC“嘿,GCC,我知道我在做什么!”。这将允许GCC将a*a*a*a*a*a转换为一段不同的文本-它看起来与a*a*a*a*a*a*a*b*a不同-但仍在a*a a*a a*a*a a*的错误间隔内计算一个数字。这是可以的,因为你已经知道你使用的是时间间隔,而不是理想的数字。

还没有海报提到浮动表达式的收缩(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

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的模幂,因此即使在溢出的情况下也可以自由地重新排序。

另一个类似的情况是:大多数编译器不会将a+b+c+d优化为(a+b)+(c+d)(这是一种优化,因为第二个表达式可以更好地进行流水线处理),并按照给定的方式对其求值(即(((a+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));

这将输出1.00000e-05 0.000000e+00