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

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

其他回答

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

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

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

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

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

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

Fortran(为科学计算而设计)有一个内置的幂运算符,据我所知,Fortran编译器通常会以与您描述的方式类似的方式优化整数幂的提升。不幸的是,C/C++没有幂运算符,只有库函数pow()。这并不妨碍智能编译器专门处理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);
}

为好奇的人澄清:这并没有找到计算幂的最佳方法,但由于找到最佳解是一个NP完全问题,而且这只值得对小幂做(而不是使用pow),因此没有理由大惊小怪。

然后将其用作功率<6>(a)。

这样可以很容易地输入幂(不需要像用括号一样拼出6),并且可以在不使用数学的情况下进行这种优化,以防出现精度相关的情况,例如补偿求和(这是一个操作顺序至关重要的示例)。

您可能也会忘记这是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

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

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

对于功率<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

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

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