为什么 clang 为这个涉及求幂的简单函数生成的代码比 gcc 快得多?

Why does clang produce a much faster code than gcc for this simple function involving exponentiation?

以下使用 clang 编译的代码运行速度几乎是使用 gcc 编译的代码的 60 倍,并且具有相同的编译器标志(-O2-O3):

#include <iostream>
#include <math.h> 
#include <chrono>
#include <limits>

long double func(int num)
{
    long double i=0;
    long double k=0.7;

    for(int t=1; t<num; t++){
      for(int n=1; n<16; n++){
        i += pow(k,n);
      }
    }
    return i;
}


int main()
{
   volatile auto num = 3000000; // avoid constant folding

   std::chrono::time_point<std::chrono::system_clock> start, end;
   start = std::chrono::system_clock::now();

   auto i = func(num);

   end = std::chrono::system_clock::now();
   std::chrono::duration<double> elapsed = end-start;
   std::cout.precision(std::numeric_limits<long double>::max_digits10);
   std::cout << "Result " << i << std::endl;
   std::cout << "Elapsed time is " << elapsed.count() << std::endl;

   return 0;
}

我用三个 gcc 版本 4.8.4/4.9.2/5.2.1 和两个 clang 版本 3.5.1/3.6.1 测试了这个,这是我机器上的时间(gcc 5.2.1clang 3.6.1):

计时-O3:

gcc:    2.41888s
clang:  0.0396217s 

计时-O2:

gcc:    2.41024s
clang:  0.0395114s 

计时-O1:

gcc:    2.41766s
clang:  2.43113s

所以看起来 gcc 即使在更高的优化级别上也根本没有优化这个功能。 clang 的汇编输出几乎比 gcc 长 100 行左右,我认为没有必要在这里 post 它,我只能说在 gcc 程序集输出有一个对 pow 的调用,它没有出现在 clang 程序集中,大概是因为 clang 将它优化为一堆内部调用。

由于结果相同(即i = 6966764.74717416727754),问题是:

  1. 为什么 gcc 可以不优化此功能,而 clang 可以?
  2. k的值改为1.0gcc变得一样快,是否存在gcc无法绕过的浮点运算问题?

我确实尝试 static_casting 并打开警告以查看隐式转换是否存在任何问题,但实际上没有。

更新: 为了完整起见,这里是 -Ofast

的结果
gcc:    0.00262204s
clang:  0.0013267s

关键是gcc没有优化O2/O3处的代码。

由此 godbolt session clang 能够在编译时执行所有 pow 计算。它在编译时知道 kn 的值是什么,它只是不断地折叠计算:

.LCPI0_0:
    .quad   4604480259023595110     # double 0.69999999999999996
.LCPI0_1:
    .quad   4602498675187552091     # double 0.48999999999999994
.LCPI0_2:
    .quad   4599850558606658239     # double 0.34299999999999992
.LCPI0_3:
    .quad   4597818534454788671     # double 0.24009999999999995
.LCPI0_4:
    .quad   4595223380205512696     # double 0.16806999999999994
.LCPI0_5:
    .quad   4593141924544133109     # double 0.11764899999999996
.LCPI0_6:
    .quad   4590598673379842654     # double 0.082354299999999963
.LCPI0_7:
    .quad   4588468774839143248     # double 0.057648009999999972
.LCPI0_8:
    .quad   4585976388698138603     # double 0.040353606999999979
.LCPI0_9:
    .quad   4583799016135705775     # double 0.028247524899999984
.LCPI0_10:
    .quad   4581356477717521223     # double 0.019773267429999988
.LCPI0_11:
    .quad   4579132580613789641     # double 0.01384128720099999
.LCPI0_12:
    .quad   4576738892963968780     # double 0.0096889010406999918
.LCPI0_13:
    .quad   4574469401809764420     # double 0.0067822307284899942
.LCPI0_14:
    .quad   4572123587912939977     # double 0.0047475615099429958

并展开内循环:

.LBB0_2:                                # %.preheader
    faddl   .LCPI0_0(%rip)
    faddl   .LCPI0_1(%rip)
    faddl   .LCPI0_2(%rip)
    faddl   .LCPI0_3(%rip)
    faddl   .LCPI0_4(%rip)
    faddl   .LCPI0_5(%rip)
    faddl   .LCPI0_6(%rip)
    faddl   .LCPI0_7(%rip)
    faddl   .LCPI0_8(%rip)
    faddl   .LCPI0_9(%rip)
    faddl   .LCPI0_10(%rip)
    faddl   .LCPI0_11(%rip)
    faddl   .LCPI0_12(%rip)
    faddl   .LCPI0_13(%rip)
    faddl   .LCPI0_14(%rip)

请注意,它使用的是内置函数(gcc documents theirs here) to calculate pow at compile time and if we use -fno-builtin 它不再执行此优化。

如果将 k 更改为 1.0 那么 gcc is able to perform 同样的优化:

.L3:
    fadd    %st, %st(1) #,
    addl    , %eax    #, t
    cmpl    %eax, %edi  # t, num
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    fadd    %st, %st(1) #,
    jne .L3 #,

虽然是比较简单的情况。

如果将内循环的条件更改为 n < 4,则 gcc seems willing to optimizek = 0.7。正如对该问题的评论中所指出的,如果编译器不相信展开会有所帮助,那么它可能会保守地考虑它会进行多少展开,因为存在代码大小权衡。

如评论中所述,我在 godbolt 示例中使用了 OP 代码的修改版本,但它不会改变基本结论。

注释如 if we use -fno-math-errno, which stops errno from being set, gcc does apply a similar optimization 所示。

除了 Shafik Yaghmour 的回答之外,我想指出的是,您对变量 num 使用 volatile 似乎没有效果的原因是 numfunc 甚至被调用之前被读取。读取不能被优化掉,但是函数调用仍然可以被优化掉。如果您将 func 的参数声明为对 volatile 的引用,即。 long double func(volatile int& num),这会阻止编译器优化掉对 func.

的整个调用