我只是在C++
和Fortran
中使用递归函数,我意识到Fortran
中的简单递归函数几乎是其等效C++
的两倍。功能。现在,在开始讨论之前,我知道这里有类似的问题,特别是:
然而,由于Fortran编译器似乎正在使用asm volatile
中的gcc
执行的操作,因此我更具体和困惑。为了给你一些上下文,让我们考虑以下递归Fibonacci number
实现:
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
和尾部跳转。 / p>
现在,如果我只是在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
看不到这个&#34;优化&#34;? C
或C++
中更快地进行递归(不依赖于内联汇编或迭代式编码)?可能是变形模板? 更新:
gcc 4.8.4
。我也尝试使用gcc 4.9.2
和gcc 5.2
进行编译,得到相同的结果。asm
我声明输入参数变为volatile (volatile int n)
而不是(const int n)
,虽然这会导致一点点我机器上的运行时间稍慢。-fno-optimize-sibling-calls
标志来解决此问题。由于此标记在-O2
级别及更高级别激活,因此即使使用-O1
进行编译也可以解决此问题。clang 3.5.1
与-O3 -march=native
运行相同的示例,虽然情况不一样,clang
似乎也会生成asm
更快的代码。 Clang Timing:
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
答案 0 :(得分:4)
请参阅本答案末尾附近的粗体字,了解如何获得由gcc生成的快速程序。阅读答案以回答这四个问题。
您的第一个问题假设gfortran
能够看到gcc
未能看到的优化可能性。事实上,情况正好相反。 gcc
确定了一些被认为是优化可能性的内容,而gfortran
错过了它。唉,gcc
错了,它应用的优化结果是你的系统100%速度损失(与我的相当)。
解决第二个问题:asm
语句阻止了内部转换,使gcc
看到了错误的优化可能性。如果没有asm
语句,您的代码就会(有效地)转换为:
int fib(const int n)
{
if (n < 2)
return n;
else
return fib(n-1) + fib(n-2);
}
包含递归调用的return语句触发&#34;兄弟调用优化&#34;这会使你的代码变得悲观。包含asm语句会阻止在其上移动返回指令。
目前,我手头只有gcc,所以我无法通过证据来尝试其他编译器的行为来回答你的第三个问题,但这似乎肯定是编译器依赖的。你遇到了一个gcc的怪癖(或者你称之为bug),它在尝试优化它时会产生错误的代码。不同编译器的优化器差异很大,因此其他编译器很可能不像gcc
那样错误地优化您的代码。另一方面,优化的代码转换是一个研究得很好的主题,大多数编译器都在实现类似的优化方法,因此有可能另一个编译器进入与gcc
相同的陷阱。
要解决你的最后一个问题:这不是C / C ++与Fortan的问题,而是关于gcc
的一个问题,它会混淆这个示例程序(以及可能类似的生产程序)。因此,无法在C++
中更快地进行递归,但有一种方法可以通过禁用gcc
来加速此示例有问题的优化: -fno-optimize-sibling-calls
,这导致(在我的系统上,在单个测试运行中)代码甚至比仅插入asm
语句更快的代码。