内联虚函数(Clang vs GCC)

时间:2016-06-17 19:06:59

标签: optimization x86 g++ clang

拿这个愚蠢的例子:

class Base
{
  public:
    virtual void ant() { i++; };
    virtual void dec() { i--; };
  int i;
};

void function(Base * base) 
{ 
  base->ant(); 
  base->dec(); 
}

我想象这是由编译器实现的方式是两个虚函数调用。 Clang就是这样做的(使用尾调用来调用dec()): -

function(Base*):                      # @function(Base*)
        push    rbx
        mov     rbx, rdi
        mov     rax, qword ptr [rbx]
        call    qword ptr [rax]
        mov     rax, qword ptr [rbx]
        mov     rdi, rbx
        pop     rbx
        jmp     qword ptr [rax + 8]     # TAILCALL
另一方面,GCC远远超出了部分内联函数调用的方式:

Base::ant():
        add     DWORD PTR [rdi+8], 1      # this_2(D)->i,
        ret
Base::dec():
        sub     DWORD PTR [rdi+8], 1      # this_2(D)->i,
        ret
function(Base*):
        push    rbx     #
        mov     rax, QWORD PTR [rdi]      # _3, base_2(D)->_vptr.Base
        mov     rbx, rdi  # base, base
        mov     rdx, QWORD PTR [rax]      # _4, *_3
        cmp     rdx, OFFSET FLAT:Base::ant()      # _4,
        jne     .L4       #,
        mov     rax, QWORD PTR [rax+8]    # _7, MEM[(int (*__vtbl_ptr_type) () *)prephitmp_8 + 8B]
        add     DWORD PTR [rdi+8], 1      # base_2(D)->i,
        cmp     rax, OFFSET FLAT:Base::dec()      # _7,
        jne     .L6       #,
.L12:
        sub     DWORD PTR [rbx+8], 1      # base_2(D)->i,
        pop     rbx       #
        ret
.L4:
        call    rdx     # _4
        mov     rax, QWORD PTR [rbx]      # _3, base_2(D)->_vptr.Base
        mov     rax, QWORD PTR [rax+8]    # _7, MEM[(int (*__vtbl_ptr_type) () *)prephitmp_8 + 8B]
        cmp     rax, OFFSET FLAT:Base::dec()      # _7,
        je      .L12        #,
.L6:
        mov     rdi, rbx  #, base
        pop     rbx       #
        jmp     rax       # _7

这真的有利吗?为什么?它看起来像是相同数量的内存查找,但在混合中抛出了额外的比较。也许我的例子太琐碎了。

有关您可以使用的版本,请参阅godbolt

1 个答案:

答案 0 :(得分:2)

gcc假设两个可预测的分支比两个间接调用便宜。如果事实证明它猜对了,并且内联的调用是应该发生的调用,那肯定是正确的。配置文件引导优化(-fprofile-generate / -fprofile-use)可能会检测是否不是这种情况,并且如果合适,推测性地内联其他内容。 (或者,也许不是那么聪明)。

间接分支可能非常昂贵,因为分支预测器必须正确预测目标地址。如果它们是可预测的,那么两个未采取的分支是非常便宜的。我有点惊讶g​​cc没有检查两个地址是否为基类,在这种情况下根本没有触及i(因为inc和dec取消了)。 )

现代CPU非常擅长快速浏览大量指令,而在这种情况下有相当数量的指令级并行性。请参阅Agner Fog's microarch guide标记wiki中的和其他链接。