但是,我更具体和困惑的是Fortran编译器似乎正在做你可以通过gcc中的asm volatile
实现的事情。为了给您一些上下文,让我们考虑以下递归的Fibonacci数实现:
Fortran 代码:
module test
implicit none
private
public fib
contains
! Fibonacci function
integer recursive function fib(n) result(r)
integer, intent(in) :: n
if (n < 2) then
r = n
else
r = fib(n-1) + fib(n-2)
end if
end function ! end of Fibonacci function
end module
program fibonacci
use test, only: fib
implicit none
integer :: r,i
integer :: n = 1e09
real(8) :: start, finish, cum_time
cum_time=0
do i= 1,n
call cpu_time(start)
r = fib(20)
call cpu_time(finish)
cum_time = cum_time + (finish - start)
if (cum_time >0.5) exit
enddo
print*,i,'runs, average elapsed time is', cum_time/i/1e-06, 'us'
end program
编译方式:
gfortran -O3 -march=native
C++ 代码:
#include <iostream>
#include <chrono>
using namespace std;
// Fib function
int fib(const int n)
{
int r;
if (n < 2)
r = n;
else
r = fib(n-1) + fib(n-2);
return r;
} // end of fib
template<typename T, typename ... Args>
double timeit(T (*func)(Args...), Args...args)
{
double counter = 1.0;
double mean_time = 0.0;
for (auto iter=0; iter<1e09; ++iter){
std::chrono::time_point<std::chrono::system_clock> start, end;
start = std::chrono::system_clock::now();
func(args...);
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
mean_time += elapsed_seconds.count();
counter++;
if (mean_time > 0.5){
mean_time /= counter;
std::cout << static_cast<long int>(counter)
<< " runs, average elapsed time is "
<< mean_time/1.0e-06 << " \xC2\xB5s" << std::endl;
break;
}
}
return mean_time;
}
int main(){
timeit(fib,20);
return 0;
}
编译时使用的:
g++ -O3 -march=native
时间:
Fortran: 24991 runs, average elapsed time is 20.087 us
C++ : 12355 runs, average elapsed time is 40.471 µs
所以,gfortran
比gcc
快了两倍。查看汇编代码,我得到了以下结果:
汇编(Fortran):
.L28:
cmpl $1, %r13d
jle .L29
leal -8(%rbx), %eax
movl %ecx, 12(%rsp)
movl %eax, 48(%rsp)
leaq 48(%rsp), %rdi
leal -9(%rbx), %eax
movl %eax, 16(%rsp)
call __bench_MOD_fib
leaq 16(%rsp), %rdi
movl %eax, %r13d
call __bench_MOD_fib
movl 12(%rsp), %ecx
addl %eax, %r13d
汇编语言(C++):
.L28:
movl 72(%rsp), %edx
cmpl $1, %edx
movl %edx, %eax
jle .L33
subl $3, %eax
movl $0, 52(%rsp)
movl %eax, %esi
movl %eax, 96(%rsp)
movl 92(%rsp), %eax
shrl %eax
movl %eax, 128(%rsp)
addl %eax, %eax
subl %eax, %esi
movl %edx, %eax
subl $1, %eax
movl %esi, 124(%rsp)
movl %eax, 76(%rsp)
这两个汇编代码由几乎相似的块/标签重复组成。如您所见,Fortran汇编对fib
函数进行了两次调用,而在C++汇编中,gcc
可能已经展开了所有递归调用,这可能需要更多的堆栈push/pop
和尾部跳转。
现在,如果我只在C++代码中加入一个内联汇编注释,如下所示:
修改后的C++代码:
// Fib function
int fib(const int n)
{
int r;
if (n < 2)
r = n;
else
r = fib(n-1) + fib(n-2);
asm("");
return r;
} // end of fib
生成的汇编代码,以及对其进行的修改。 汇编代码(C++修改版):
.L7:
cmpl $1, %edx
jle .L17
leal -4(%rbx), %r13d
leal -5(%rbx), %edx
cmpl $1, %r13d
jle .L19
leal -5(%rbx), %r14d
cmpl $1, %r14d
jle .L55
leal -6(%rbx), %r13d
movl %r13d, %edi
call _Z3fibi
leal -7(%rbx), %edi
movl %eax, %r15d
call _Z3fibi
movl %r13d, %edi
addl %eax, %r15d
现在你可以看到两个对fib
函数的调用。通过计时它们,我得到了以下结果:
计时:
Fortran: 24991 runs, average elapsed time is 20.087 us
C++ : 25757 runs, average elapsed time is 19.412 µs
我知道使用没有输出的
asm
和asm volatile
的效果是抑制编译器的激进优化,但在这种情况下,gcc
认为它太聪明了,结果一开始就生成了不太高效的代码。所以问题是:
- 为什么
gcc
看不到这个“优化”,而gfortan
明显能看到?
- 内联汇编行必须在返回语句之前。将其放在其他地方将没有效果。为什么?
- 这种行为是否特定于编译器?例如,你能否用clang/MSVC模仿相同的行为?
- 在C或C++中有更安全的方法使递归更快(不依赖于内联汇编或迭代式编码)吗?也许是变长模板?更新:
- 上面显示的结果都是使用
gcc 4.8.4
得出的。我还尝试了使用gcc 4.9.2
和gcc 5.2
进行编译,并得到了相同的结果。
- 如果将asm
替换为声明输入参数为volatile,即(volatile int n)
而不是(const int n)
,则该问题也可以复制(修复)。尽管这会在我的机器上导致稍微慢一点的运行时间。
- 正如Michael Karcher所提到的,我们可以传递-fno-optimize-sibling-calls
标志来解决此问题。由于这个标志在-O2
级别及以上被激活,即使使用-O1
编译也会解决此问题。
- 我已经使用clang 3.5.1
和-O3 -march=native
运行了相同的示例,尽管情况不完全相同,但clang
似乎也会生成更快的代码与asm
一起使用。Clang计时:
clang++ w/o asm : 8846 runs, average elapsed time is 56.4555 µs
clang++ with asm : 10427 runs, average elapsed time is 47.8991 µs
-fdump-tree-optimized
和-fdump-tree-original
,并在其中搜索差异。也许还可以使用-fdump-tree-inlined
。 - Vladimir F Героям слава