提问者:小点点

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


使用clang编译的以下代码比使用具有相同编译器标志(-O2-O3)的gcc编译的代码运行速度快近60倍:

#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行左右,我认为没有必要在这里发布它,我只能说在gcc汇编输出中有一个对pow的调用,它不会出现在clang汇编中,大概是因为clang将其优化为一堆内在调用。

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

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

我确实尝试了static_casting并打开了警告,看看隐式转换是否有任何问题,但不是真的。

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

gcc:    0.00262204s
clang:  0.0013267s

关键是gcc不会优化O2/O3处的代码。


共2个答案

匿名用户

从这个golbolt会话中,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在此处记录了他们的函数)在编译时计算pow,如果我们使用-fno-builtin,它将不再执行此优化。

如果您将k更改为1.0,那么gcc能够执行相同的优化:

.L3:
    fadd    %st, %st(1) #,
    addl    $1, %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

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

请注意,如上面的注释所示,如果我们使用-fno-math-errno,它会阻止设置errno,gcc确实应用了类似的优化。

匿名用户

除了Shafik Yaghmour的回答,我想指出的是,你在变量num上使用易失性看起来没有任何效果的原因是num甚至在func被调用之前就被读取了。读取不能被优化掉,但函数调用仍然可以被优化掉。如果你声明func的参数是对易失性的引用,即。long双func(易失性int