我只是在玩递归函数C++
and Fortran
我意识到一个简单的递归函数Fortran
几乎是同类产品的两倍C++
功能。现在,在讨论这个问题之前,我知道这里也有类似的问题,具体来说:
- 为什么添加汇编注释会导致生成的代码发生如此根本的变化?
- 工作的asm volatile(““ : : : “记忆”)
- 相当于gfortran中的asm volatility
然而,我有点更具体和困惑,因为 Fortran 编译器似乎正在做你可以实现的事情asm volatile
in 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
Timing:
Fortran: 24991 runs, average elapsed time is 20.087 us
C++ : 12355 runs, average elapsed time is 40.471 µs
So 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
功能。给他们计时让我
Timing:
Fortran: 24991 runs, average elapsed time is 20.087 us
C++ : 25757 runs, average elapsed time is 19.412 µs
我知道效果asm
没有输出并且asm volatile
是为了抑制激进的编译器优化,但在这种情况下,gcc
认为它太聪明了,但最终生成了效率较低的代码。
所以问题是:
- Why can
gcc
没有看到这个“优化”,当gfortan
显然可以吗?
- 内联装配线必须位于 return 语句之前。放在别处就没有效果了。为什么?
- 此行为是编译器特定的吗?例如,您可以使用 clang/MSVC 模仿相同的行为吗?
- 有没有安全的方法可以使递归更快
C
or C++
(不依赖内联汇编或迭代式编码)?也许可变参数模板?
UPDATE:
- 上面显示的结果都是
gcc 4.8.4
。我也尝试过用它来编译它gcc 4.9.2
and gcc 5.2
我得到相同的结果。
- 这个问题也可以被复制(修复?)如果不是把
asm
我将输入参数声明为易失性,即(volatile int n)
代替(const int n)
,尽管这会导致我的机器上的运行时间稍微慢一些。
- As 迈克尔·凯彻已经提到过,我们可以通过
-fno-optimize-sibling-calls
标志来解决这个问题。由于该标志被激活于-O2
级别及以上,甚至编译-O1
解决了这个问题。
- 我已经运行了相同的示例
clang 3.5.1
with -O3 -march=native
尽管情况并不完全相同,clang
似乎还可以生成更快的代码asm
.
铿锵时间:
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