Funciones y rendimiento virtuales – C ++

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

Una buena regla empírica es:

No es un problema de rendimiento hasta que puedas 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. Mejores lugares para buscar mejoras de rendimiento están en algoritmos y E / S.

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

Tu pregunta me hizo sentir curiosidad, así que seguí adelante y realicé algunos tiempos en la CPU PowerPC de 3GHz con la que trabajamos. La prueba que realicé fue para hacer una clase simple de vector 4d 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 arreglos, cada uno de los cuales contenía 1024 de estos vectores (lo suficientemente pequeños como para caber en L1) y ejecuté un ciclo que los sumba entre sí (Ax = Bx + Cx) 1000 veces. Ejecuté esto con las funciones definidas como llamadas de función en inline , virtual y regulares. Aquí están los resultados:

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

Entonces, en este caso (donde todo cabe en 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 causó exactamente 3 * 4 * 1024 = 12,288 llamadas de función (1024 vectores multiplicados por cuatro componentes por tres llamadas por adición), por lo que estos tiempos representan 1000 * 12,288 = 12,288,000 llamadas de función. El bucle virtual tomó 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 funciones directas, y no , a menos que planee llamarlas diez millones de veces por segundo, no importa.

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

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

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

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

Una llamada de función virtual primero necesita cargar el puntero vtable desde el objeto. Esto puede provocar que se pierda un caché de datos. A continuación, carga el puntero a la función desde el vtable, lo que puede ocasionar que falte otro caché de datos. Luego llama a la función que puede dar como resultado que falte un 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 círculo cerrado en el código de rendimiento crítico, puede reducir drásticamente el rendimiento.

Desde la página 44 del manual “Optimizar software en C ++” de Agner Fog :

El tiempo que se tarda en llamar a una función de miembro virtual es de unos pocos ciclos de reloj más de lo que se necesita para llamar a una función miembro no virtual, siempre que la instrucción de llamada de función siempre llame a la misma versión de la función virtual. Si la versión cambia, obtendrá una penalización de mal predicción 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 conmutación …

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

Es como en los viejos tiempos cuando la gente decía que la progtwigción estructurada era lenta porque todo el código se dividía en funciones, cada función requería asignaciones de stack y una llamada a función.

La única vez que incluso pensaría en molestarme en considerar el impacto en el rendimiento de una función virtual, es si fue muy usado y ejemplificado en un código de plantilla que terminó en todo. ¡Incluso entonces, no gastaría demasiado esfuerzo en eso!

PS piensa en otros lenguajes “fáciles de usar”: todos sus métodos son virtuales y no se rastrean hoy en día.

Hay otros criterios 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 ” vinculación dinámica simulada ” en tiempo de comstackción con plantillas para obtener el efecto de “polymorphism estático”, que es algo 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 comstackció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 básicos (es decir, polymorphism en tiempo de ejecución), sino desde un sentido estático, si desea crear una clase Y que sea igual que una clase X de plantilla preexistente que tenga el ganchos para este tipo de anulación, solo tiene que anular los métodos que le interesan, y luego obtendrá los métodos básicos de clase X sin tener que tener un vtable.

En clases con grandes huellas de memoria, el costo de un único puntero vtable no es mucho, pero algunas de las clases ATL en COM son muy pequeñas, y vale la pena el ahorro de tiempo si el caso de polymorphism 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.

Sí, tienes razón y, si sientes curiosidad por el costo de la función virtual, es posible que esta publicación te parezca interesante.

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

Aunque, como otras personas han dicho, casi nunca será un problema para ti en la vida real. Y si cree que es así, ejecute un generador de perfiles, realice algunas pruebas y verifique si esto realmente es un problema antes de tratar de “desdesignar” su código para obtener un beneficio de rendimiento.

Cuando el método de la clase no es virtual, el comstackdor generalmente hace el forro interno. Por el contrario, cuando utiliza el puntero a alguna clase con función virtual, la dirección real se conocerá solo en el tiempo de ejecución.

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

 #include  

El impacto de la llamada de 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 bien, cuando se trata de una llamada virtual repetidamente utilizada muchas veces, mientras se realiza una operación simple, podría ser realmente grande.

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

¿El éxito en el rendimiento va a ser notable en una computadora portátil / computadora de escritorio / tableta moderna … probablemente no? Sin embargo, en ciertos casos con sistemas integrados, el golpe de rendimiento puede ser el factor determinante en 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 con fecha aproximada que analiza las mejores prácticas para C / C ++ en el contexto de sistemas integrados: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Para concluir: le corresponde al progtwigdor comprender los pros / contras de usar una determinada construcción sobre otra. A menos que sea súper impulsado por el rendimiento, es probable que no le importe el éxito en el rendimiento y debería usar todo lo relacionado con OO en C ++ para ayudar a que su código sea lo más utilizable posible.

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

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 comstackdor, el caché, etc.

La penalización de rendimiento de usar funciones virtuales nunca puede exceder las ventajas que obtiene en el 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 hay un nivel de indirección a través del VMT. Sin embargo, el tiempo necesario para hacer la llamada normalmente es muy pequeño en comparación con el tiempo que lleva llevar a cabo la ejecución real de su función, por lo que el costo total de rendimiento será nigligable, especialmente con el rendimiento actual del hardware. Además, el comstackdor a veces puede optimizar y ver que no se necesita una llamada virtual y comstackrla en una llamada estática. Así que no se preocupe, use las funciones virtuales y las clases abstractas tanto como lo necesite.

Siempre me he cuestionado esto, especialmente desde hace unos años, también hice una prueba comparando los tiempos de una llamada a 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 buffer, en una aplicación de gran rendimiento crítico, así que busqué en Google (y te encontré), y al final, hice la prueba nuevamente.

 // g++ -std=c++0x -o perf perf.cpp -lrt #include  // typeid #include  // printf #include  // atoll #include  // 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 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 void test(unsigned long long count) { test(count); test(count); } int main(int argc, const char* argv[]) { test(argc == 2 ? atoll(argv[1]) : 10000000000llu); return 0; } 

Y realmente me sorprendió que, de hecho, ya no importara para nada. Si bien tiene sentido tener líneas en línea más rápido que los no virtuales, y que sean más rápidos que los virtuales, a menudo se trata de la carga de la computadora en general, si su caché tiene los datos necesarios o no, y mientras que usted podría ser capaz de optimizar a nivel de caché, creo, esto debería ser hecho por los desarrolladores del comstackdor más que por los desarrolladores de aplicaciones.