为什么 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.1
和 clang 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
),问题是:
- 为什么
gcc
可以不优化此功能,而 clang
可以?
- 将
k
的值改为1.0
,gcc
变得一样快,是否存在gcc
无法绕过的浮点运算问题?
我确实尝试 static_cast
ing 并打开警告以查看隐式转换是否存在任何问题,但实际上没有。
更新: 为了完整起见,这里是 -Ofast
的结果
gcc: 0.00262204s
clang: 0.0013267s
关键是gcc
没有优化O2/O3
处的代码。
由此 godbolt session clang 能够在编译时执行所有 pow
计算。它在编译时知道 k
和 n
的值是什么,它只是不断地折叠计算:
.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 optimize 当 k = 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
似乎没有效果的原因是 num
在 func
甚至被调用之前被读取。读取不能被优化掉,但是函数调用仍然可以被优化掉。如果您将 func
的参数声明为对 volatile
的引用,即。 long double func(volatile int& num)
,这会阻止编译器优化掉对 func
.
的整个调用
以下使用 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.1
和 clang 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
),问题是:
- 为什么
gcc
可以不优化此功能,而clang
可以? - 将
k
的值改为1.0
,gcc
变得一样快,是否存在gcc
无法绕过的浮点运算问题?
我确实尝试 static_cast
ing 并打开警告以查看隐式转换是否存在任何问题,但实际上没有。
更新: 为了完整起见,这里是 -Ofast
gcc: 0.00262204s
clang: 0.0013267s
关键是gcc
没有优化O2/O3
处的代码。
由此 godbolt session clang 能够在编译时执行所有 pow
计算。它在编译时知道 k
和 n
的值是什么,它只是不断地折叠计算:
.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 optimize 当 k = 0.7
。正如对该问题的评论中所指出的,如果编译器不相信展开会有所帮助,那么它可能会保守地考虑它会进行多少展开,因为存在代码大小权衡。
如评论中所述,我在 godbolt 示例中使用了 OP 代码的修改版本,但它不会改变基本结论。
注释如 errno
from being set, gcc does apply a similar optimization 所示。
除了 Shafik Yaghmour 的回答之外,我想指出的是,您对变量 num
使用 volatile
似乎没有效果的原因是 num
在 func
甚至被调用之前被读取。读取不能被优化掉,但是函数调用仍然可以被优化掉。如果您将 func
的参数声明为对 volatile
的引用,即。 long double func(volatile int& num)
,这会阻止编译器优化掉对 func
.