Pregunta

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 se nota y parece que estoy haciendo una optimización prematura. ¿Cierto?

¿Fue útil?

Solución

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) es Punteros de funciones de miembro y los Los delegados de C ++ más rápidos posibles .

Otros consejos

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 cada una con 1024 de estos vectores (lo suficientemente pequeñas como para caber en L1) y ejecuté un ciclo que las agregó entre sí (A.x = B.x + C.x) 1000 veces. Ejecuté esto con las funciones definidas como en línea , virtual y llamadas regulares a funciones. 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,288 llamadas de función (1024 vectores por cuatro componentes por tres llamadas por adición), por lo que estos tiempos representan 1000 * 12,288 = 12,288,000 llamadas 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.

Consulte también: comparación del ensamblado 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 a 3 GHz.

En aplicaciones muy críticas para el rendimiento (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 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 pérdida de memoria caché de instrucciones 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 una pérdida de caché de datos. Luego carga el puntero de la función desde la tabla vtable, lo que puede provocar que se pierda otro 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 errores 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.

De la página 44 de Agner Fog's " Optimizing Software in C ++ " manual :

  

El tiempo que lleva llamar a una función miembro virtual es unos pocos ciclos de reloj más de lo que lleva 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 ...

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 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!

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

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 tiempo de compilación " enlace dinámico simulado " con plantillas para obtener el efecto de " polimorfismo estático " ;, que es difícil de explicar ; básicamente pasa la clase derivada como un 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 caso. 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 puntero vtable único no es mucho, pero algunas de las clases ATL en COM son muy pequeñas, y vale la pena el ahorro vtable si el caso de polimorfismo en tiempo de ejecución nunca va a ocurrir.

Consulte también esta otra pregunta SO .

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

Sí, tiene razón y si tiene curiosidad sobre el costo de la función virtual, puede encontrar esta publicación 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 ciclo cerrado, y si y solo si causan un error de página o otro " pesado " operación de memoria para ocurrir.

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 un beneficio de rendimiento.

Cuando el método de la clase no es virtual, el compilador generalmente lo hace en línea. Por el contrario, cuando utiliza 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.

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, los éxitos de rendimiento todavía existen 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 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, 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.

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 debe estar en línea, entonces no puede hacer que la función sea virtual porque eso lo impediría. 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 " mayo " porque esto depende del compilador, el caché, etc.

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 suele ser muy pequeño en comparación con el tiempo necesario para 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.

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 vacío las llamadas virtuales son 8 veces más lentas que las 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, ya no importa en absoluto. 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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top