gcc的asm volatile是否等同于递归的gfortran默认设置?

时间:2015-10-06 16:08:00

标签: c++ gcc optimization assembly fortran

我只是在C++Fortran中使用递归函数,我意识到Fortran中的简单递归函数几乎是其等效C++的两倍。功能。现在,在开始讨论之前,我知道这里有类似的问题,特别是:

  1. Why does adding assembly comments cause such radical change in generated code?
  2. Working of asm volatile (“” : : : “memory”)
  3. Equivalent to asm volatile in gfortran
  4. 然而,由于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;?
    • 内联装配线必须在return语句之前。把它放在别处,它将没有任何效果。为什么?
    • 此行为编译器是否具体?例如,你能用clang / MSVC模仿相同的行为吗?
    • 是否有更安全的方法可以在CC++中更快地进行递归(不依赖于内联汇编或迭代式编码)?可能是变形模板?

    更新

    • 上面显示的结果都是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 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 
    

1 个答案:

答案 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语句更快的代码。