Funciones virtuales y rendimiento-C++


En el diseño de mi clase, uso 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. ¿Verdad?

Author: Scott Stensland, 2009-01-16

15 answers

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 al rendimiento general de su aplicación. Los mejores lugares para buscar mejoras de rendimiento están en algoritmos y E/S.

Un excelente artículo que habla sobre funciones virtuales (y más) es Los Punteros de Funciones miembro y el C++más Rápido posible Delegados.

 82
Author: Greg Hewgill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 08:25:56

Su pregunta me hizo curioso, así que me adelanté y ejecuté algunos tiempos en la CPU PowerPC de 3 GHz en orden con la que trabajamos. La prueba que ejecuté fue hacer una clase vectorial 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 cada una 1024 de estos vectores (lo suficientemente pequeños como para caber en L1) y ejecuté un bucle que los agregaba entre sí (A. x = B. x + C. x) 1000 veces. Ejecuté esto con las funciones definidas como inline, virtual, y llamadas a funciones regulares. Aquí están los resultados:

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

Por lo tanto, 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 llamadas a la función 3 * 4 * 1024 = 12,288 (1024 vectores por cuatro componentes por tres llamadas por adición), por lo que estos tiempos representan llamadas a la función 1000 * 12,288 = 12,288,000. El 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 esté planeando llamarlas diez millones de veces por segundo, no importa.

Véase también: comparación del ensamblado generado.

 151
Author: Crashworks,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-22 19:07:09

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

 38
Author: Chuck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 08:56:41

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

Una llamada a una función normal puede generar un error 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 a una función virtual primero necesita cargar la vtable puntero del objeto. Esto puede resultar en un error de caché de datos. Luego carga el puntero de la función de la vtable que puede resultar en otro error de caché de datos. Luego llama a la función que puede resultar en una falta 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 bucle apretado en el código crítico de rendimiento puede reducir drásticamente el rendimiento.

 30
Author: Mark James,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-17 08:25:49

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

El tiempo que se tarda en llamar a una función miembro virtual es unos pocos ciclos de reloj más de lo que se tarda en llamar a una función miembro no virtual, siempre que la instrucción function call siempre llame a la misma versión de la función virtual. Si la versión cambia, recibirá una penalización por error de 10 a 30 ciclos de reloj. Las reglas para la predicción y la mala predicción de llamadas a funciones virtuales son igual que para las instrucciones switch...

 26
Author: Boojum,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 10:08:53

Absolutamente. Era un problema cuando las computadoras se ejecutaban a 100MHz, ya que cada llamada a un método requería una búsqueda en la vtable antes de ser llamada. Pero hoy.. en una CPU de 3GHz que tiene caché de 1er nivel con más memoria que la que tenía mi primera computadora? Para nada. Asignar memoria desde la RAM principal le costará más tiempo que si todas sus funciones fueran virtuales.

Es como los viejos, 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 una función requiere asignaciones de pila y una llamada a la función!

La única vez que me molestaría en considerar el impacto en el rendimiento de una función virtual, es si fue muy utilizada e instanciada en código templado 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 bajo la cubierta y no se arrastran hoy en día.

 6
Author: gbjbaanb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 08:28:43

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

En clases con grandes huellas de memoria, el costo de un solo puntero vtable no es mucho, pero algunos de los ATL las clases 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í está una publicación que encontré que habla sobre los aspectos de rendimiento en tiempo de CPU.

 6
Author: Jason S,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:26:32

Sí, tienes razón y si tienes curiosidad sobre el costo de la llamada a la función virtual, puede que encuentres este post interesante.

 4
Author: Serge,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 08:31:53

La única manera en que puedo ver que una función virtual se convertirá en un problema de rendimiento es si muchas funciones virtuales se llaman dentro de un bucle apretado, y si y solo si causan un error de página u otra operación de memoria "pesada" que ocurra.

Aunque como otras personas han dicho que casi nunca va a ser un problema para usted en la vida real. Y si usted piensa que es, ejecutar un generador de perfiles, hacer algunas pruebas, y verificar si esto realmente es un problema antes de tratar de" no diseñar " su código para un beneficio de rendimiento.

 3
Author: Daemin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 08:58:46

Cuando el método de clase no es virtual, el compilador generalmente lo hace in-lining. Por el contrario, cuando utiliza 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 una 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-que podría ser insignificante.

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

 3
Author: Evgueny Sedov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-06 19:10:45

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, mantenibilidad y legibilidad, por otro lado, los éxitos de rendimiento aún existen con las funciones virtuales.

Es el éxito de rendimiento va a ser notable en un portátil moderno/escritorio/tableta... probablemente no! Sin embargo, en ciertos casos con sistemas embebidos, el impacto en el rendimiento puede ser el factor impulsor en su la ineficiencia del código, especialmente si la función virtual se llama una y otra vez en un bucle.

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

Para concluir: depende del programador entender los pros/contras de usar una determinada construcción sobre otra. A menos que estés impulsado por el súper rendimiento, probablemente no te importe el el rendimiento golpea y debe usar todas las cosas de OO en C++ para ayudar a que su código sea lo más usable posible.

 2
Author: It'sPete,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-30 23:01:59

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á la diferencia.

 2
Author: Hurkyl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-10 21:58:08

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 está llamando a una función, mientras que el segundo puede estar llamando a muchas funciones diferentes. Esto se aplica a cualquier función virtual en cualquier idioma.

Digo "may" porque esto depende del compilador, la caché, etc.

 1
Author: nikdeapen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-16 00:22:16

La penalización en el rendimiento del uso de funciones virtuales nunca puede superar las ventajas que obtiene 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 hay un nivel de indirección a través de la VMT. Sin embargo, el tiempo necesario para realizar la llamada es normalmente muy pequeño en comparación con el tiempo necesario en la ejecución real de su función, por lo que el costo total de rendimiento será nigligable, especialmente con rendimiento actual del hardware. Además, el compilador a veces puede optimizar y ver que no se necesita ninguna llamada virtual y compilarla en una llamada estática. Así que no te preocupes, usa funciones virtuales y clases abstractas tanto como necesites.

 0
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-01-16 08:41:05

Siempre me cuestioné esto, especialmente porque - hace unos años - también hice una prueba comparando los tiempos de una llamada al método miembro estándar con una virtual y estaba realmente enojado por los resultados en ese momento, teniendo llamadas virtuales vacías siendo 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 buffer, en una aplicación muy crítica de rendimiento, así que busqué en Google (y te encontré), y en el fin, hice la prueba de nuevo.

// 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 estaba realmente sorprendido de que - de hecho - realmente ya no importa en absoluto. Si bien tiene sentido tener líneas de entrada 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 podría ser capaz de optimizar a nivel de caché, creo, que esto debe ser hecho por los desarrolladores del compilador más que por los desarrolladores de aplicaciones.

 -1
Author: christianparpart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-29 18:31:07