Виртуальная функция C ++ для производного объекта проходит через vtable? - PullRequest
6 голосов
/ 16 декабря 2010

В следующем коде она вызывает виртуальную функцию 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();
}

Ответы [ 6 ]

9 голосов
/ 16 декабря 2010

Большинство компиляторов достаточно умны, чтобы исключить косвенный вызов в этом сценарии, если у вас включена оптимизация. Но только потому, что вы только что создали объект, а компилятор знает динамический тип; могут быть ситуации, когда вы знаете динамический тип, а компилятор - нет.

6 голосов
/ 16 декабря 2010

Как обычно, ответ на этот вопрос звучит так: «Если это важно для вас, взгляните на выданный код».Это то, что 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>
4 голосов
/ 29 декабря 2010

Это скомпилированный код из g ++ (4.5) с -O3

_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 ++ на самом деле довольно плох в оптимизации вызовов виртуальных функций.

3 голосов
/ 16 декабря 2010

Да, он будет использовать vtable (только не виртуальные методы обходят vtable). Чтобы позвонить B::foo() на b напрямую, позвоните b->B::foo().

1 голос
/ 16 декабря 2010

Компилятор может оптимизировать виртуальную диспетчеризацию и напрямую вызывать виртуальную функцию или встроить ее, если он может доказать, что это то же самое поведение.В приведенном примере компилятор легко отбрасывает каждую строку кода, поэтому все, что вы получите, это:

int main() {}
0 голосов
/ 18 сентября 2012

Я немного изменил код, чтобы испытать его сам, и мне кажется, что он отбрасывает 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

И интересные фрагменты выглядят для меня так, будто версия opt отбрасывает vtable. Похоже, он создает vtable, но не использует его.

В опции:

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 мы видим в комментариях: _vptr.A, что, я уверен, означает, что он выполняет поиск в vtable, однако в реальной основной функции он, похоже, способен предсказать ответ и даже не вызовите этот код использования:

 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

На этот раз основная функция вставляет копию 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 и т.п. и ничего не нашел).
  • Но g ++ с -O6 может провести большую оптимизацию простой программы с очевидными константами до такой степени, что она может предсказать результат и вообще пропустить вызов.
  • Однако, когда все становится сложным (читай реальным), он определенно выполняет поиск в vtable, почти каждый раз, когда вы вызываете виртуальную функцию.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...