¿Alguien puede explicar en detalle cómo funciona exactamente la tabla virtual y qué punteros están asociados cuando se llaman funciones virtuales?
Si en realidad son más lentos, ¿puede mostrar que el tiempo que tarda la función virtual en ejecutarse es más que los métodos de clase normales? Es fácil perder la noción de cómo / qué está sucediendo sin ver algún código.
Respuestas:
Los métodos virtuales se implementan comúnmente a través de las llamadas tablas de métodos virtuales (vtable para abreviar), en las que se almacenan los punteros de función. Esto agrega indirección a la llamada real (debe buscar la dirección de la función para llamar desde la tabla virtual, luego llamarla, en lugar de simplemente llamarla inmediatamente). Por supuesto, esto lleva algo de tiempo y algo más de código.
Sin embargo, no es necesariamente la causa principal de la lentitud. El verdadero problema es que el compilador (generalmente / usualmente) no puede saber qué función se llamará. Por lo tanto, no puede alinearlo ni realizar ninguna otra optimización de este tipo. Esto solo podría agregar una docena de instrucciones sin sentido (preparar registros, llamar y luego restaurar el estado), y podría inhibir otras optimizaciones aparentemente no relacionadas. Además, si se ramifica como loco al llamar a muchas implementaciones diferentes, sufre los mismos golpes que sufriría si se ramifica como loco por otros medios: el caché y el predictor de ramificación no lo ayudarán, las ramificaciones tardarán más de lo que es perfectamente predecible rama.
Grande pero : estos éxitos de rendimiento suelen ser demasiado pequeños para importar. Vale la pena considerar si desea crear un código de alto rendimiento y considerar agregar una función virtual que se llamaría con una frecuencia alarmante. Sin embargo, también tenga en cuenta que reemplazar las llamadas a funciones virtuales con otros medios de ramificación (
if .. else
,switch
punteros de función, etc.) no resolverá el problema fundamental, puede muy bien ser más lento. El problema (si es que existe) no son funciones virtuales sino indirección (innecesaria).Editar: la diferencia en las instrucciones de la llamada se describe en otras respuestas. Básicamente, el código para una llamada estática ("normal") es:
Una llamada virtual hace exactamente lo mismo, excepto que la dirección de la función no se conoce en tiempo de compilación. En cambio, un par de instrucciones ...
En cuanto a las ramas: una rama es cualquier cosa que salta a otra instrucción en lugar de simplemente dejar que se ejecute la siguiente instrucción. Esto incluye
if
,switch
, partes de varios bucles, llamadas a funciones, etc. ya veces los implementos compilador cosas que no parecen rama de una manera que realmente necesita una rama bajo el capó. Consulte ¿Por qué el procesamiento de una matriz ordenada es más rápido que una matriz sin clasificar? por qué esto puede ser lento, qué hacen las CPU para contrarrestar esta desaceleración y cómo esto no es una cura para todo.fuente
virtual
.Aquí hay un código desmontado real de una llamada de función virtual y una llamada no virtual, respectivamente:
Puede ver que la llamada virtual requiere tres instrucciones adicionales para buscar la dirección correcta, mientras que la dirección de la llamada no virtual se puede compilar.
Sin embargo, tenga en cuenta que la mayoría de las veces ese tiempo de búsqueda adicional puede considerarse insignificante. En situaciones en las que el tiempo de búsqueda sería significativo, como en un ciclo, el valor generalmente se puede almacenar en caché haciendo las tres primeras instrucciones antes del ciclo.
La otra situación en la que el tiempo de búsqueda se vuelve significativo es si tiene una colección de objetos y está realizando una llamada virtual a una función virtual en cada uno de ellos. Sin embargo, en ese caso, necesitará algunos medios para seleccionar qué función llamar de todos modos, y una búsqueda de tabla virtual es un medio tan bueno como cualquier otro. De hecho, dado que el código de búsqueda de vtable se usa tanto, está muy optimizado, por lo que tratar de evitarlo manualmente tiene una buena posibilidad de resultar en un peor rendimiento.
fuente
-0x8(%rbp)
. oh mi ... esa sintaxis de AT&T.¿Más lento que qué ?
Las funciones virtuales resuelven un problema que no puede resolverse mediante llamadas a funciones directas. En general, solo puede comparar dos programas que computan lo mismo. "Este rastreador de rayos es más rápido que ese compilador" no tiene sentido, y este principio se generaliza incluso a cosas pequeñas como funciones individuales o construcciones de lenguaje de programación.
Si no utiliza una función virtual para cambiar dinámicamente a un fragmento de código basado en un dato, como el tipo de un objeto, tendrá que usar otra cosa, como una
switch
declaración para lograr lo mismo. Ese algo más tiene sus propios gastos generales, más implicaciones en la organización del programa que influyen en su capacidad de mantenimiento y rendimiento global.Tenga en cuenta que en C ++, las llamadas a funciones virtuales no siempre son dinámicas. Cuando las llamadas se realizan en un objeto cuyo tipo exacto se conoce (porque el objeto no es un puntero o referencia, o porque su tipo puede inferirse estáticamente), las llamadas son solo llamadas de funciones miembro regulares. Eso no solo significa que no hay gastos generales de envío, sino también que estas llamadas pueden alinearse de la misma manera que las llamadas normales.
En otras palabras, su compilador de C ++ puede funcionar cuando las funciones virtuales no requieren un despacho virtual, por lo que generalmente no hay razón para preocuparse por su rendimiento en relación con las funciones no virtuales.
Nuevo: Además, no debemos olvidar las bibliotecas compartidas. Si está utilizando una clase que está en una biblioteca compartida, la llamada a una función miembro ordinaria no será simplemente una secuencia de instrucciones agradable como
callq 0x4007aa
. Tiene que pasar por algunos aros, como indirectamente a través de una "tabla de enlaces de programa" o alguna estructura similar. Por lo tanto, la indirección de la biblioteca compartida podría nivelar (si no completamente) la diferencia de costo entre una llamada virtual (verdaderamente indirecta) y una llamada directa. Por lo tanto, el razonamiento sobre las compensaciones de funciones virtuales debe tener en cuenta cómo se construye el programa: si la clase del objeto de destino está vinculada monolíticamente al programa que está realizando la llamada.fuente
porque una llamada virtual es equivalente a
donde con una función no virtual el compilador puede doblar constantemente la primera línea, esta es una desreferencia, una adición y una llamada dinámica transformada en solo una llamada estática
esto también le permite alinear la función (con todas las debidas consecuencias de optimización)
fuente