Funciones virtuales y rendimiento - C ++

125

En mi diseño de clase, uso clases abstractas y funciones virtuales ampliamente. Tenía la sensación de que las funciones virtuales afectan el rendimiento. ¿Es esto cierto? Pero creo que esta diferencia de rendimiento no es notable y parece que estoy haciendo una optimización prematura. ¿Correcto?

Navaneeth KN
fuente
Según mi respuesta, sugiero cerrar esto como un duplicado de stackoverflow.com/questions/113830
Suma
posible duplicado de la penalización
Bo Persson
2
Si está haciendo cómputo de alto rendimiento y procesamiento de números, no use ninguna virtualidad en el núcleo del cálculo: definitivamente mata todas las actuaciones y evita optimizaciones en tiempo de compilación. Para la inicialización o finalización del programa no es importante. Cuando trabaje con interfaces, puede usar la virtualidad como lo desee.
Vincent

Respuestas:

90

Una buena regla general es:

No es un problema de rendimiento hasta que pueda probarlo.

El uso de funciones virtuales tendrá un efecto muy leve en el rendimiento, pero es poco probable que afecte el rendimiento general de su aplicación. Los mejores lugares para buscar mejoras de rendimiento son los algoritmos y las E / S.

Un excelente artículo que habla sobre funciones virtuales (y más) son los punteros de funciones de miembros y los delegados de C ++ más rápidos posibles .

Greg Hewgill
fuente
¿Qué pasa con las funciones virtuales puras? ¿Afectan el rendimiento de alguna manera? Simplemente me pregunto, ya que parece que están allí simplemente para imponer la implementación.
thomthom
2
@thomthom: Correcto, no hay diferencia de rendimiento entre las funciones virtuales puras y virtuales.
Greg Hewgill
168

Su pregunta me hizo sentir curiosidad, así que seguí adelante y ejecuté algunos tiempos en la CPU PowerPC en orden de 3 GHz con la que trabajamos. La prueba que ejecuté fue hacer una clase de vector 4d simple con funciones get / set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Luego configuré tres matrices que contenían 1024 de estos vectores (lo suficientemente pequeñas como para caber en L1) y ejecuté un ciclo que las agregó entre sí (Ax = Bx + Cx) 1000 veces. Me encontré con esto con las funciones definidas como inline, virtualy llamadas a funciones regulares. Aquí están los resultados:

  • en línea: 8 ms (0.65ns por llamada)
  • directo: 68ms (5.53ns por llamada)
  • virtual: 160ms (13ns por llamada)

Entonces, en este caso (donde todo cabe en la memoria caché), las llamadas a funciones virtuales fueron aproximadamente 20 veces más lentas que las llamadas en línea. Pero, ¿qué significa esto realmente? Cada viaje a través del bucle provocó exactamente 3 * 4 * 1024 = 12,288llamadas a funciones (1024 vectores por cuatro componentes por tres llamadas por adición), por lo que estos tiempos representan 1000 * 12,288 = 12,288,000llamadas a funciones. El bucle virtual tardó 92 ms más que el bucle directo, por lo que la sobrecarga adicional por llamada fue de 7 nanosegundos por función.

De esto concluyo: , las funciones virtuales son mucho más lentas que las directas, y no , a menos que esté planeando llamarlas diez millones de veces por segundo, no importa.

Ver también: comparación del conjunto generado.

Crashworks
fuente
Pero si se les llama varias veces, a menudo pueden ser más baratos que cuando solo se llaman una vez. Vea mi blog irrelevante: phresnel.org/blog , las publicaciones tituladas "Funciones virtuales consideradas no dañinas", pero por supuesto depende de la complejidad de sus rutas de código
Sebastian Mach
22
Mi prueba mide un pequeño conjunto de funciones virtuales llamadas repetidamente. Su publicación de blog supone que el costo de tiempo del código se puede medir contando las operaciones, pero eso no siempre es cierto; El costo principal de un vfunc en los procesadores modernos es la burbuja de la tubería causada por un error de predicción de la rama.
Crashworks el
10
este sería un gran punto de referencia para gcc LTO (Optimización del tiempo de enlace); intente compilar esto nuevamente con lto habilitado: gcc.gnu.org/wiki/LinkTimeOptimization y vea qué sucede con el factor 20x
lurscher
1
Si una clase tiene una función virtual y una en línea, ¿también se verá afectado el rendimiento del método no virtual? ¿Simplemente por la naturaleza de la clase siendo virtual?
thomthom
44
@thomthom No, virtual / no virtual es un atributo por función. Una función solo necesita definirse a través de vtable si está marcada como virtual o si está anulando una clase base que la tiene como virtual. A menudo verá clases que tienen un grupo de funciones virtuales para la interfaz pública, y luego muchos accesores en línea, etc. (Técnicamente, se trata de la aplicación específica y un compilador podría utilizar ponters virtuales incluso para funciones marcadas 'inline', pero una persona que escribió un compilador de este tipo sería una locura.)
Crashworks
42

Cuando Objective-C (donde todos los métodos son virtuales) es el idioma principal para el iPhone y Java es el idioma principal para Android, creo que es bastante seguro usar funciones virtuales C ++ en nuestras torres de doble núcleo de 3 GHz.

Arrojar
fuente
44
No estoy seguro de que el iPhone sea un buen ejemplo de código de rendimiento: youtube.com/watch?v=Pdk2cJpSXLg
Crashworks
13
@Crashworks: el iPhone no es un ejemplo de código en absoluto. Es un ejemplo de hardware, específicamente hardware lento , que es el punto que estaba haciendo aquí. Si estos lenguajes supuestamente "lentos" son lo suficientemente buenos para hardware con poca potencia, las funciones virtuales no van a ser un gran problema.
Chuck
52
El iPhone se ejecuta en un procesador ARM. Los procesadores ARM utilizados para iOS están diseñados para bajo uso de MHz y bajo consumo de energía. No hay silicio para la predicción de bifurcación en la CPU y, por lo tanto, no hay sobrecarga de rendimiento por fallas en la predicción de bifurcación de las llamadas a funciones virtuales. Además, el hardware de MHz para iOS es lo suficientemente bajo como para que una pérdida de caché no detenga el procesador durante 300 ciclos de reloj mientras recupera datos de la RAM. Las fallas de caché son menos importantes a MHz más bajo. En resumen, no hay gastos generales por el uso de funciones virtuales en dispositivos iOS, pero este es un problema de hardware y no se aplica a las CPU de los equipos de escritorio.
HaltingState
44
Como programador de Java desde hace mucho tiempo recién ingresado a C ++, quiero agregar que el compilador JIT de Java y el optimizador de tiempo de ejecución tienen la capacidad de compilar, predecir e incluso integrar algunas funciones en tiempo de ejecución después de un número predefinido de bucles. Sin embargo, no estoy seguro de si C ++ tiene esa característica en el momento de la compilación y el enlace porque carece de un patrón de llamada en tiempo de ejecución. Por lo tanto, en C ++ podríamos tener que ser un poco más cuidadosos.
Alex Suo
@ AlexSuo ¿No estoy seguro de tu punto? Al compilarse, C ++, por supuesto, no puede optimizar en función de lo que podría suceder en tiempo de ejecución, por lo que la propia CPU tendría que hacer predicciones, etc. tiempo de ejecución
underscore_d
34

En aplicaciones críticas de rendimiento (como los videojuegos), una llamada a la función virtual puede ser demasiado lenta. Con el hardware moderno, la mayor preocupación de rendimiento es la pérdida de caché. Si los datos no están en el caché, pueden pasar cientos de ciclos antes de que estén disponibles.

Una llamada de función normal puede generar una falta de caché de instrucciones cuando la CPU obtiene la primera instrucción de la nueva función y no está en la caché.

Una llamada de función virtual primero necesita cargar el puntero vtable desde el objeto. Esto puede resultar en una pérdida de caché de datos. Luego carga el puntero de la función de la tabla vtable que puede resultar en otra pérdida de caché de datos. Luego llama a la función que puede resultar en un error de caché de instrucciones como una función no virtual.

En muchos casos, dos fallas de caché adicionales no son una preocupación, pero en un ciclo cerrado en el código crítico de rendimiento puede reducir drásticamente el rendimiento.

Mark James
fuente
66
Correcto, pero cualquier código (o vtable) que se llame repetidamente desde un bucle cerrado (por supuesto) rara vez sufrirá errores de caché. Además, el puntero vtable está típicamente en la misma línea de caché que otros datos en el objeto al que accederá el método llamado, por lo que a menudo estamos hablando de solo una falta de caché adicional.
Qwertie
55
@Qwertie No creo que sea necesariamente cierto. El cuerpo del bucle (si es más grande que el caché L1) podría "retirar" el puntero vtable, el puntero de función y la iteración posterior tendrían que esperar el acceso al caché L2 (o más) en cada iteración
Ghita
30

De la página 44 del manual "Optimización del software en C ++" de Agner Fog :

El tiempo que lleva llamar a una función miembro virtual es unos pocos ciclos de reloj más de lo que toma llamar a una función miembro no virtual, siempre que la instrucción de llamada a la función siempre llame a la misma versión de la función virtual. Si la versión cambia, obtendrá una penalización de predicción errónea de 10 a 30 ciclos de reloj. Las reglas para la predicción y la predicción errónea de las llamadas a funciones virtuales son las mismas que para las declaraciones de cambio ...

Boojum
fuente
Gracias por esta referencia Los manuales de optimización de Agner Fog son el estándar de oro para utilizar el hardware de manera óptima.
Arto Bendiken
Según mi recuerdo y una búsqueda rápida - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - Dudo que esto sea siempre cierto switch. Con casevalores totalmente arbitrarios , claro. Pero si todos los cases son consecutivos, un compilador podría optimizar esto en una tabla de salto (ah, eso me recuerda a los viejos días de Z80), que debería ser (por falta de un término mejor) tiempo constante. No es que recomiendo tratar de reemplazar vfuncs con switch, lo cual es ridículo. ;)
underscore_d
7

absolutamente. Era un problema cuando las computadoras funcionaban a 100Mhz, ya que cada llamada al método requería una búsqueda en la tabla antes de que se llamara. Pero hoy ... ¿en una CPU de 3Ghz que tiene caché de primer nivel con más memoria que mi primera computadora? De ningún modo. Asignar memoria de la RAM principal le costará más tiempo que si todas sus funciones fueran virtuales.

Es como en los viejos tiempos, donde la gente decía que la programación estructurada era lenta porque todo el código se dividía en funciones, ¡cada función requería asignaciones de pila y una llamada a la función!

La única vez que pensaría en molestarme en considerar el impacto en el rendimiento de una función virtual es si se usa mucho y se instancia en un código con plantilla que terminó en todo. ¡Incluso entonces, no gastaría demasiado esfuerzo en ello!

PD: piense en otros lenguajes 'fáciles de usar': todos sus métodos son virtuales y no se arrastran hoy en día.

gbjbaanb
fuente
44
Bueno, incluso hoy, evitar las llamadas a funciones es importante para las aplicaciones de alto rendimiento. La diferencia es que los compiladores de hoy incorporan funciones pequeñas de manera confiable para que no suframos penalizaciones de velocidad por escribir funciones pequeñas. En cuanto a las funciones virtuales, las CPU inteligentes pueden hacer predicciones de ramas inteligentes en ellas. Creo que el hecho de que las computadoras antiguas fueran más lentas no es realmente el problema: sí, eran mucho más lentas, pero en aquel entonces lo sabíamos, así que les dimos cargas de trabajo mucho más pequeñas. En 1992, si reproducíamos un MP3, sabíamos que podríamos tener que dedicar más de la mitad de la CPU a esa tarea.
Qwertie
66
mp3 data de 1995. en 92 apenas teníamos 386, de ninguna manera podían reproducir un mp3, y el 50% del tiempo de la CPU supone un buen sistema operativo multitarea, un proceso inactivo y un programador preventivo. Nada de esto existía en el mercado de consumo en ese momento. fue 100% desde el momento en que se encendió la energía, final de la historia.
v.oddou
7

Hay otro criterio de rendimiento además del tiempo de ejecución. Un Vtable también ocupa espacio en la memoria y, en algunos casos, se puede evitar: ATL utiliza " enlace dinámico simulado " en tiempo de compilación con plantillaspara obtener el efecto del "polimorfismo estático", que es difícil de explicar; básicamente pasa la clase derivada como parámetro a una plantilla de clase base, por lo que en el momento de la compilación la clase base "sabe" cuál es su clase derivada en cada instancia. No le permitirá almacenar múltiples clases derivadas diferentes en una colección de tipos base (es decir, polimorfismo en tiempo de ejecución), pero desde un sentido estático, si desea hacer una clase Y que sea igual a una clase de plantilla X preexistente que tiene el ganchos para este tipo de anulación, solo necesita anular los métodos que le interesan, y luego obtiene los métodos básicos de la clase X sin tener que tener una vtable.

En clases con grandes huellas de memoria, el costo de un solo puntero de vtable no es mucho, pero algunas de las clases de ATL en COM son muy pequeñas, y vale la pena el ahorro de vtable si el caso de polimorfismo en tiempo de ejecución nunca va a ocurrir.

Ver también esta otra pregunta SO .

Por cierto, aquí hay una publicación que encontré que habla sobre los aspectos de rendimiento del tiempo de CPU.

Jason S
fuente
1
Se llama polimorfismo paramétrico
tjysdsg
4

Sí, tienes razón y si tienes curiosidad sobre el costo de la función virtual, puedes encontrar esta publicación interesante.

Sarga
fuente
1
El artículo vinculado no considera una parte muy importante de la llamada virtual, y esa es una posible predicción de la rama.
Suma el
4

La única forma en que puedo ver que una función virtual se convertirá en un problema de rendimiento es si se invocan muchas funciones virtuales dentro de un ciclo cerrado, y si y solo si causan una falla de página u otra operación de memoria "pesada".

Aunque, como han dicho otras personas, nunca será un problema para ti en la vida real. Y si cree que es así, ejecute un generador de perfiles, haga algunas pruebas y verifique si esto realmente es un problema antes de intentar "no diseñar" su código para obtener un beneficio de rendimiento.

Daemin
fuente
2
llamar a cualquier cosa en un circuito cerrado es probable que mantenga todo ese código y datos calientes en la memoria caché ...
Greg Rogers
2
Sí, pero si ese bucle derecho está iterando a través de una lista de objetos, entonces cada objeto podría estar llamando a una función virtual en una dirección diferente a través de la misma llamada a la función.
Daemin 02 de
3

Cuando el método de clase no es virtual, el compilador generalmente lo hace en línea. Por el contrario, cuando usa el puntero a alguna clase con función virtual, la dirección real solo se conocerá en tiempo de ejecución.

Esto está bien ilustrado por prueba, diferencia de tiempo ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

El impacto de la llamada a función virtual depende en gran medida de la situación. Si hay pocas llamadas y una cantidad significativa de trabajo dentro de la función, podría ser insignificante.

O, cuando se trata de una llamada virtual que se usa repetidamente muchas veces, mientras se realiza una operación simple, podría ser realmente grande.

Evgueny Sedov
fuente
44
Una llamada de función virtual es cara en comparación con ++ia. ¿Y qué?
Bo Persson
2

He ido y venido en esto al menos 20 veces en mi proyecto particular. Aunque puede haber algunas grandes ganancias en términos de reutilización de código, claridad, facilidad de mantenimiento y legibilidad, por otro lado, los éxitos en el rendimiento aún lo hacen existir con funciones virtuales.

¿El rendimiento será notable en una computadora portátil / computadora de escritorio / tableta moderna ... probablemente no! Sin embargo, en ciertos casos con sistemas integrados, el impacto en el rendimiento puede ser el factor determinante de la ineficiencia de su código, especialmente si la función virtual se llama una y otra vez en un bucle.

Aquí hay un documento anticuado que analiza las mejores prácticas para C / C ++ en el contexto de los sistemas integrados: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Para concluir: depende del programador comprender las ventajas y desventajas de utilizar una determinada construcción sobre otra. A menos que esté impulsado por el rendimiento súper, probablemente no le importe el impacto en el rendimiento y debería usar todas las cosas ordenadas de OO en C ++ para ayudar a que su código sea lo más utilizable posible.

EsPete
fuente
2

En mi experiencia, lo más relevante es la capacidad de alinear una función. Si tiene necesidades de rendimiento / optimización que dictan que una función debe estar en línea, entonces no puede hacer que la función sea virtual porque eso lo evitaría. De lo contrario, probablemente no notarás la diferencia.


fuente
1

Una cosa a tener en cuenta es que esto:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

puede ser más rápido que esto:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

Esto se debe a que el primer método solo llama a una función, mientras que el segundo puede llamar a muchas funciones diferentes. Esto se aplica a cualquier función virtual en cualquier idioma.

Digo "puede" porque esto depende del compilador, el caché, etc.

Nikdeapen
fuente
0

La penalización de rendimiento del uso de funciones virtuales nunca puede superar las ventajas que obtienes a nivel de diseño. Supuestamente, una llamada a una función virtual sería un 25% menos eficiente que una llamada directa a una función estática. Esto se debe a que existe un nivel de indirección a través de VMT. Sin embargo, el tiempo necesario para realizar la llamada normalmente es muy pequeño en comparación con el tiempo empleado en la ejecución real de su función, por lo que el costo total de rendimiento será insignificante, especialmente con el rendimiento actual del hardware. Además, el compilador a veces puede optimizar y ver que no se necesita una llamada virtual y compilarlo en una llamada estática. Así que no se preocupe, use funciones virtuales y clases abstractas tanto como lo necesite.


fuente
2
nunca, no importa cuán pequeña sea la computadora de destino?
zumalifeguard
Podría haber aceptado si lo hubieras expresado como The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.La diferencia clave dice sometimes, no never.
underscore_d
-1

Siempre me cuestioné esto, especialmente porque, hace unos años, también hice una prueba de este tipo comparando los tiempos de una llamada de método de miembro estándar con una virtual y estaba realmente enojado por los resultados en ese momento, teniendo llamadas virtuales vacías. 8 veces más lento que los no virtuales.

Hoy tuve que decidir si usar o no una función virtual para asignar más memoria en mi clase de búfer, en una aplicación muy crítica para el rendimiento, así que busqué en Google (y te encontré), y al final, volví a hacer la prueba.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

Y realmente me sorprendió que, de hecho, realmente ya no importa. Si bien tiene sentido tener líneas en línea más rápidas que las no virtuales, y que sean más rápidas que las virtuales, a menudo se trata de la carga de la computadora en general, ya sea que su caché tenga los datos necesarios o no, y si bien puede optimizar a nivel de caché, creo que esto debería ser realizado por los desarrolladores del compilador más que por los desarrolladores de aplicaciones.

christianparpart
fuente
12
Creo que es muy probable que su compilador pueda decir que la llamada a la función virtual en su código solo puede llamar a Virtual :: call. En ese caso, solo puede alinearlo. Tampoco hay nada que impida que el compilador alinee Normal :: call aunque no se lo haya pedido. Así que creo que es muy posible que obtengas los mismos tiempos para las 3 operaciones porque el compilador está generando un código idéntico para ellas.
Bjarke H. Roune