gcc 的 asm volatile 和 gfortran 的默认递归设置是否相同?

8
我刚刚在C++和Fortran中尝试递归函数,发现Fortran中的简单递归函数几乎比其等效的C++函数快两倍。在进入此问题之前,我知道这里有类似的问题,具体如下:
  1. 为什么添加汇编注释会导致生成的代码发生如此巨大的变化?
  2. asm volatile("":::"memory")的工作原理
  3. 在gfortran中相当于asm volatile的内容

但是,我更具体和困惑的是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

所以,gfortrangcc快了两倍。查看汇编代码,我得到了以下结果:

汇编(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

我知道使用没有输出的asmasm volatile的效果是抑制编译器的激进优化,但在这种情况下,gcc认为它太聪明了,结果一开始就生成了不太高效的代码。
所以问题是:
- 为什么gcc看不到这个“优化”,而gfortan明显能看到? - 内联汇编行必须在返回语句之前。将其放在其他地方将没有效果。为什么? - 这种行为是否特定于编译器?例如,你能否用clang/MSVC模仿相同的行为? - 在C或C++中有更安全的方法使递归更快(不依赖于内联汇编或迭代式编码)吗?也许是变长模板?
更新:
- 上面显示的结果都是使用gcc 4.8.4得出的。我还尝试了使用gcc 4.9.2gcc 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 

3
非常有趣的发现。我将继续关注其他人在这里所说的话。 - SergeyA
2
我建议首先使用-fdump-tree-optimized-fdump-tree-original,并在其中搜索差异。也许还可以使用-fdump-tree-inlined - Vladimir F Героям слава
1
使用哪个版本的gcc?对应于C++程序的汇编似乎不完整。 - Marc Glisse
1个回答

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);
}

包含递归调用的返回语句会触发“兄弟调用优化”,使您的代码变慢。加入asm语句可以防止将返回指令移动到它之前。

目前,我手头只有gcc,所以无法通过证据来尝试其他编译器的行为来回答您的第三个问题,但这显然是与编译器相关的。您遇到了gcc的怪癖(或错误,无论您如何称呼它),在尝试优化它时生成了糟糕的代码。不同编译器的优化器非常不同,因此很可能其他编译器不像gcc那样误优化您的代码。另一方面,为了优化而进行的代码转换是一个经过深入研究的主题,大多数编译器正在实施类似的优化方法,因此可能会有其他编译器陷入与gcc相同的陷阱。

回答您的最后一个问题:这不是关于C/C++与Fortan的问题,而是关于gcc混乱了这个示例程序(以及可能类似的生产程序)的问题。因此,没有办法让C++中的递归更快,但有一种方法可以加快这个示例,在gcc中禁用有问题的优化:-fno-optimize-sibling-calls,这在我的系统上进行了单次测试运行后,结果甚至比仅插入asm语句更快。


1
我已经向gcc报告了这个错误优化问题,链接为https://gcc.gnu.org/bugzilla/show_bug.cgi?id=68008。 - Michael Karcher

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接