2010-12-16 13 views
6

En el siguiente código, llama a una función virtual foo a través de un puntero a un objeto derivado. ¿Esta llamada pasará por el vtable o llamará directamente al B::foo?¿La llamada de función virtual de C++ al objeto derivado pasa por vtable?

Si va a través de un vtable, ¿cuál sería una forma idiomática de C++ de hacer que llame al B::foo directamente? Sé que en este caso siempre estoy apuntando a un B.

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

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


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

¿Está tratando de optimizar (no pierda su tiempo que es el trabajo de los compiladores). ¿O quieres una técnica para simplemente llamar a la versión de B de foo()? –

+3

No debería preocuparse si el envío será directo o irá a través de un vtable. En la mayoría de los escenarios, el envío de tablas de métodos virtuales casi nunca tendrá un impacto significativo en el rendimiento. –

Respuesta

3

Sí, usará el vtable (solo los métodos no virtuales omiten el vtable). Para llamar directamente al B::foo() en b, llame al b->B::foo().

+3

Para el código en la pregunta, la mayoría de los compiladores de optimización no usarán la tabla v, la mayoría alineará el cuerpo vacío, y la tabla v en sí misma puede ser eliminada por el enlazador ya que no se usa. –

+2

@Ben Voigt sí, eso es muy posible. Supongo que el código que está mirando el cartel original es mucho más complicado, y ese puede no ser el caso. – robert

+0

Gracias, esta sintaxis es lo que me faltaba. Soy consciente de los otros problemas relacionados con optimizaciones, etc. – aaa

9

La mayoría de los compiladores serán lo suficientemente inteligentes como para eliminar la llamada indirecta en ese escenario, si tiene la optimización habilitada. Pero solo porque acaba de crear el objeto y el compilador conoce el tipo dinámico; Puede haber situaciones en las que conozca el tipo dinámico y el compilador no.

+0

En cuyo caso puede usar 'static_cast' para forzar que se conozca el tipo dinámico ..... –

+3

@Billy: No estoy tan seguro. 'static_cast' solo le dice al compilador que el tipo dinámico es una subclase (no estricta) de' B', no que es exactamente 'B', por lo que la optimización no aplica IMO. –

+0

@Billy: ¿Por qué intentar engañar al compilador? El compilador ya conoce más que un humano sobre el código y cómo optimizarlo. Solo deja que haga su trabajo. –

6

Como es habitual, la respuesta a esta pregunta es "si es importante para usted, eche un vistazo al código emitido". Esto es lo que produce g ++ sin optimizaciones seleccionadas:

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 

que está utilizando el vtable. Una llamada directa, producida por un código como:

B b; 
b.foo(); 

se ve así:

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

Ahora, compila con las optimizaciones y mira el código otra vez ... –

+0

@David Indeed. ¿Pero qué optimizaciones? Mi punto es que necesitas mirar el código para usar optimizaciones de manera eficiente. –

+1

@Unquiet: G ++, entonces optimizaciones -O2 u -O3. Además, diré lo que dije a todo el mundo diciendo "mira la asamblea": no todos sabemos ensamblar, y suponiendo que todos los que usan un lenguaje de nivel superior como C++ saben que es algo irracional. –

1

compilador puede optimizar la distancia de despacho virtual y llamar a la función virtual directamente o en línea si se puede probar que es el mismo comportamiento. En el ejemplo proporcionado, compilador tirar fácilmente lejos cada línea de código, por lo que todo lo que se obtiene es la siguiente:

int main() {} 
+1

El compilador no puede eliminar la llamada a nuevo.Tiene efectos secundarios que el compilador no puede anular, ya que genera llamadas a la biblioteca subyacente para la asignación de memoria. –

4

Este es el código compilado desde g ++ (4.5) con -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 

La única optimización que hizo fue que sabía qué tabla de acceso usar (en el objeto b). De lo contrario, "call * _ZTV1B + 16 (% rip)" hubiera sido "movq (% rax),% rax; call * (% rax)". Así que g ++ es bastante malo para optimizar las llamadas a funciones virtuales.

+0

GCC 4.6 y posteriores producen 'call \t _ZN1B3fooEv' así que desvirtualice con éxito y llame a' B :: foo() 'directamente (http://goo.gl/wxcSiw) –

+0

También veo eso. Pero ¿por qué no puede optimizar (en línea) la llamada de función ya que el cuerpo del método de B :: foo está vacío ... – Emil

+0

GCC 4.7+ lo alinea, solo 4.6 no (tenga en cuenta que el enlace en mi comentario anterior no definió el miembro, específicamente por lo que no se optimizaría y se mostraría la llamada) –

0

He cambiado el código un poco para probarlo, y para mí parece que está dejando caer el vtable, pero no soy lo suficientemente experto como para contarlo. Estoy seguro que algunos comentaristas me van a arreglar sin embargo :)

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

que luego se convierte este código para el montaje de esta manera:

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 

y los bits interesantes miran a mí como el 'opt' versión está soltando el vtable Parece que se está creando la viable, pero no usarlo ..

En el asm opt:

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 

y la base.Versión asm de la misma:

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 

En la línea 93 que vemos en los comentarios: _vptr.A cual estoy bastante seguro de que quiere decir que está haciendo una búsqueda vtable, sin embargo, en la función principal real, que parece ser capaz de predecir la respuesta y ni siquiera llamar a ese código Useit:

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

que creo que es sólo decir, sabemos que vamos a retorno 2, permite a ponerlo en EAX. (Ejecuté el programa pidiéndole que devolviera 200, y esa línea se actualizó como era de esperar).


poco más

Así que complica el programa un poco más:

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

En esta versión, el código Useit definitivamente utiliza la viable en el montaje optimizado:

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 

Esta vez, la función principal en línea una copia de useIt, pero realmente hace la búsqueda vtable.


¿Qué hay de C++ 11 y la palabra clave 'final'?

Así que cambió una línea a:

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

y la línea compilador:

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

Pensando que decirle al compilador que se trata de una anulación definitiva, permitiría que salte la vtable tal vez.

Resulta que todavía usa el vtable.


Así que mi respuesta teórica sería:

  • No creo que hay alguna, "no usar la viable" optimizaciones explícitos. (Busqué en la página de manual de g ++ vtable y virt y similares y no encontré nada).
  • Pero g ++ con -O6, puede hacer una gran cantidad de optimización en un programa simple con constantes obvias hasta el punto en que puede predecir el resultado y omitir la llamada por completo.
  • Sin embargo, una vez que las cosas se vuelven complejas (léales), definitivamente está haciendo búsquedas vtable, prácticamente cada vez que llamas a una función virtual.
Cuestiones relacionadas