【问题标题】:Does C++ virtual function call on derived object go through vtable?派生对象上的 C++ 虚函数调用是否通过 vtable?
【发布时间】:2010-12-16 18:29:31
【问题描述】:

在以下代码中,它通过指向派生对象的指针调用虚函数 foo。这个调用是通过vtable还是直接调用B::foo

如果它通过 vtable,那么 C++ 的惯用方式是什么让它直接调用 B::foo?我知道在这种情况下我总是指向B

Class A
{
    public:
        virtual void foo() {}
};

class B : public A
{
    public:
        virtual void foo() {}
};


int main()
{
    B* b = new B();
    b->foo();
}

【问题讨论】:

  • 您是否正在尝试优化(不要浪费您的时间,这是编译器的工作)。或者你想要一种技术来调用 B 的 foo() 版本?
  • 你不应该真正担心调度是直接的还是通过vtable。在大多数情况下,虚拟方法表调度几乎不会对性能产生重大影响。

标签: c++ virtual


【解决方案1】:

如果您启用了优化,大多数编译器将足够智能以消除这种情况下的间接调用。但这仅仅是因为您刚刚创建了对象并且编译器知道动态类型;可能存在您知道动态类型而编译器不知道的情况。

【讨论】:

  • 在这种情况下,您可以使用static_cast 强制知道动态类型.....
  • @Billy:我不太确定。 static_cast 只是告诉编译器动态类型是B 的(非严格)子类,而不是完全是B,因此优化不适用于IMO。
  • @Billy:为什么要欺骗编译器。编译器已经比人类更了解代码以及如何优化它。让它发挥作用。
  • 我同意@Ben,如果要调用特定版本,则必须使用限定函数名(如p->B::foo()pB 类型或派生自B (static_cast 如果实际指针指向基址,则可能需要static_cast)。static_cast 本身没有帮助,因为编译器不会知道您转换为的类型是否是 final覆盖者与否。
  • @Ben, @David: 如果我没记错的话,C++0x 引入了final 属性(不是我很喜欢这个想法...),我想这会开启一些优化这类。
【解决方案2】:

像往常一样,这个问题的答案是“如果它对您很重要,请查看发出的代码”。这是 g++ 在没有选择优化的情况下产生的结果:

18     b->foo();
0x401375 <main+49>:  mov    eax,DWORD PTR [esp+28]
0x401379 <main+53>:  mov    eax,DWORD PTR [eax]
0x40137b <main+55>:  mov    edx,DWORD PTR [eax]
0x40137d <main+57>:  mov    eax,DWORD PTR [esp+28]
0x401381 <main+61>:  mov    DWORD PTR [esp],eax
0x401384 <main+64>:  call   edx

它正在使用 vtable。直接调用,由如下代码生成:

B b;
b.foo();

看起来像这样:

0x401392 <main+78>:  lea    eax,[esp+24]
0x401396 <main+82>:  mov    DWORD PTR [esp],eax
0x401399 <main+85>:  call   0x40b2d4 <_ZN1B3fooEv>

【讨论】:

  • @大卫确实。但是哪些优化?我的观点是,您需要查看代码才能有效地使用优化。
  • @Unquiet: G++,所以要么 -O2 要么 -O3 优化。另外,我会说一下我对大家所说的“看看汇编”——并非所有人都知道汇编,假设每个使用 C++ 等高级语言的人都知道这是不合理的。
  • @Billy 如果您不了解平台汇编程序的基础知识,那么恕我直言,您没有资格成为程序员。毕竟,这不完全是火箭科学。
  • @unquiet mind:有趣的是我认识 几个 程序员,没有一个人知道汇编程序。对于绝大多数人来说,这真的没有必要。
  • @Billy 我也认识很多不懂汇编的“程序员”。但鲟鱼定律也适用于程序员。
【解决方案3】:

是的,它将使用 vtable(只有非虚拟方法绕过 vtable)。要直接在b 上拨打B::foo(),请拨打b-&gt;B::foo()

【讨论】:

  • 对于问题中的代码,不仅大多数优化编译器不会使用 v-table,大多数会内联空主体,而且 v-table 本身可能会被链接器消除,因为它不是没用过。
  • @Ben Voigt 是的,这很有可能。我认为原始发布者正在查看的代码要复杂得多,但事实可能并非如此。
  • 谢谢,这个语法是我所缺少的。我知道与优化等相关的其他问题。
【解决方案4】:

这是使用 -O3 从 g++ (4.5) 编译的代码

_ZN1B3fooEv:
    rep
    ret

main:
    subq    $8, %rsp
    movl    $8, %edi
    call    _Znwm
    movq    $_ZTV1B+16, (%rax)
    movq    %rax, %rdi
    call    *_ZTV1B+16(%rip)
    xorl    %eax, %eax
    addq    $8, %rsp
    ret

_ZTV1B:
    .quad   0
    .quad   _ZTI1B
    .quad   _ZN1B3fooEv

它所做的唯一优化是它知道要使用哪个 vtable(在 b 对象上)。否则“call *_ZTV1B+16(%rip)”将是“movq (%rax), %rax; call *(%rax)”。 所以g++实际上在优化虚函数调用方面做得很差。

【讨论】:

  • GCC 4.6 及更高版本产生call _ZN1B3fooEv 所以去虚拟化成功并直接调用B::foo() (goo.gl/wxcSiw)
  • 我也看到了。但是为什么它不能优化(内联)函数调用,因为 B::foo 的方法体是空的......
  • GCC 4.7+ 确实内联了它,只是 4.6 没有(请注意,我之前评论中的链接没有定义成员,特别是它不会被优化掉,调用将是显示)
【解决方案5】:

编译器可以优化掉虚拟调度并直接调用虚拟函数或内联它,如果它可以证明它是相同的行为。在提供的示例中,编译器会很容易地丢弃每一行代码,所以你将得到的是:

int main() {}

【讨论】:

  • 不允许编译器移除对 new 的调用。它具有编译器无法分析的副作用,因为它会导致调用底层库以进行内存分配。
【解决方案6】:

我稍微修改了代码以自己尝试一下,在我看来,它似乎正在删除 vtable,但我在 asm 方面不够专业,无法判断。我敢肯定,一些评论员会让我正确:)

struct A {
    virtual int foo() { return 1; }
};

struct B : public A {
    virtual int foo() { return 2; }
};

int useIt(A* a) {
    return a->foo();
}

int main()
{
    B* b = new B();
    return useIt(b);
}

然后我将这段代码转换成这样的程序集:

g++ -g -S -O0  -fverbose-asm virt.cpp 
as -alhnd virt.s > virt.base.asm
g++ -g -S -O6  -fverbose-asm virt.cpp 
as -alhnd virt.s > virt.opt.asm

在我看来,有趣的部分就像“选择”版本正在删除 vtable。看起来它正在创建 vtable 但没有使用它..

在 opt asm 中:

9:virt.cpp      **** int useIt(A* a) { 
89                    .loc 1 9 0 
90                    .cfi_startproc 
91                .LVL2: 
10:virt.cpp      ****     return a->foo(); 
92                    .loc 1 10 0 
93 0000 488B07        movq    (%rdi), %rax    # a_1(D)->_vptr.A, a_1(D)->_vptr.A 
94 0003 488B00        movq    (%rax), %rax    # *D.2259_2, *D.2259_2 
95 0006 FFE0          jmp *%rax   # *D.2259_2 
96                .LVL3: 
97                    .cfi_endproc 

和base.asm版本一样:

  9:virt.cpp      **** int useIt(A* a) { 
  88                    .loc 1 9 0 
  89                    .cfi_startproc 
  90 0000 55            pushq   %rbp    # 
  91                .LCFI6: 
  92                    .cfi_def_cfa_offset 16 
  93                    .cfi_offset 6, -16 
  94 0001 4889E5        movq    %rsp, %rbp  #, 
  95                .LCFI7: 
  96                    .cfi_def_cfa_register 6 
  97 0004 4883EC10      subq    $16, %rsp   #, 
  98 0008 48897DF8      movq    %rdi, -8(%rbp)  # a, a 
  10:virt.cpp      ****     return a->foo(); 
  99                    .loc 1 10 0 
 100 000c 488B45F8      movq    -8(%rbp), %rax  # a, tmp64 
 101 0010 488B00        movq    (%rax), %rax    # a_1(D)->_vptr.A, D.2263 
 102 0013 488B00        movq    (%rax), %rax    # *D.2263_2, D.2264 
 103 0016 488B55F8      movq    -8(%rbp), %rdx  # a, tmp65 
 104 001a 4889D7        movq    %rdx, %rdi  # tmp65, 
 105 001d FFD0          call    *%rax   # D.2264 
  11:virt.cpp      **** } 
 106                    .loc 1 11 0 
 107 001f C9            leave 
 108                .LCFI8: 
 109                    .cfi_def_cfa 7, 8 
 110 0020 C3            ret 
 111                    .cfi_endproc 

在第 93 行,我们在 cmets 中看到:_vptr.A 我很确定这意味着它正在执行 vtable 查找,但是,在实际的 main 函数中,它似乎能够预测答案,甚至无法预测调用该 useIt 代码:

 16:virt.cpp      ****     return useIt(b);
 17:virt.cpp      **** }
124                    .loc 1 17 0
125 0015 B8020000      movl    $2, %eax    #,

我认为这只是说,我们知道我们会返回 2,让我们把它放在 eax 中。 (我重新运行程序要求它返回 200,并且该行已按我的预期更新)。


额外的一点

所以我把程序复杂了一点:

struct A {
    int valA;
    A(int value) : valA(value) {}
    virtual int foo() { return valA; }
};

struct B : public A {
    int valB;
    B(int value) : valB(value), A(0) {}
    virtual int foo() { return valB; }
};

int useIt(A* a) {
    return a->foo();
}

int main()
{
    A* a = new A(100);
    B* b = new B(200);
    int valA = useIt(a);
    int valB = useIt(a);
    return valA + valB;
}

在这个版本中,useIt代码肯定使用了优化程序集中的vtable:

  13:virt.cpp      **** int useIt(A* a) {
  89                    .loc 1 13 0
  90                    .cfi_startproc
  91                .LVL2:
  14:virt.cpp      ****     return a->foo();
  92                    .loc 1 14 0
  93 0000 488B07        movq    (%rdi), %rax    # a_1(D)->_vptr.A, a_1(D)->_vptr.A
  94 0003 488B00        movq    (%rax), %rax    # *D.2274_2, *D.2274_2
  95 0006 FFE0          jmp *%rax   # *D.2274_2
  96                .LVL3:
  97                    .cfi_endproc

这一次,main 函数内联了useIt 的副本,但实际上执行了 vtable 查找。


c++11 和 'final' 关键字呢?

所以我把一行改成:

virtual int foo() override final { return valB; }

和编译器行:

g++ -std=c++11 -g -S -O6  -fverbose-asm virt.cpp

认为告诉编译器这是一个最终覆盖,可能会允许它跳过 vtable。

原来它仍然使用 vtable。


所以我的理论答案是:

  • 我认为没有任何明确的“不要使用 vtable”优化。 (我在 g++ 手册页中搜索了 vtable 和 virt 等,但一无所获)。
  • 但是带有 -O6 的 g++ 可以对具有明显常量的简单程序进行大量优化,从而可以预测结果并完全跳过调用。
  • 但是,一旦事情变得复杂(读起来),它肯定会进行 vtable 查找,几乎每次调用虚函数时。

【讨论】:

    猜你喜欢
    • 2015-10-24
    • 1970-01-01
    • 2011-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-09
    • 1970-01-01
    相关资源
    最近更新 更多