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:
- Copie algunos registros en la pila, para permitir que la función llamada use esos registros.
- Copie los argumentos en ubicaciones predefinidas, para que la función llamada pueda encontrarlos independientemente de dónde se llame.
- Empuje la dirección del remitente.
- Ramifica / salta al código de la función, que es una dirección de tiempo de compilación y, por lo tanto, está codificada en el binario por el compilador / enlazador.
- Obtenga el valor de retorno de una ubicación predefinida y restaure los registros que queremos usar.
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 ...
- Obtenga el puntero vtable, que apunta a una matriz de punteros de función (direcciones de función), uno para cada función virtual, del objeto.
- Obtenga la dirección de función correcta de vtable en un registro (el índice donde se almacena la dirección de función correcta se decide en tiempo de compilación).
- Salte a la dirección en ese registro, en lugar de saltar a una dirección codificada.
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.