2011-02-02 19 views
7

Antes de encogerse ante el título duplicado, la otra pregunta no se adecuaba a lo que pido aquí (IMO). Asi que.Funciones virtuales y rendimiento C++

Realmente quiero usar funciones virtuales en mi aplicación para hacer que las cosas sean cien veces más sencillas (¿no se trata de eso?)). Pero he leído en alguna parte que llegaron a un costo de rendimiento, sin ver nada, pero el mismo viejo bombo artificial de la optimización prematura, decidí darle un giro rápido en una pequeña prueba de referencia usando:

CProfiler.cpp

#include "CProfiler.h" 

CProfiler::CProfiler(void (*func)(void), unsigned int iterations) { 
    gettimeofday(&a, 0); 
    for (;iterations > 0; iterations --) { 
     func(); 
    } 
    gettimeofday(&b, 0); 
    result = (b.tv_sec * (unsigned int)1e6 + b.tv_usec) - (a.tv_sec * (unsigned int)1e6 + a.tv_usec); 
}; 

main.cpp

#include "CProfiler.h" 

#include <iostream> 

class CC { 
    protected: 
    int width, height, area; 
    }; 

class VCC { 
    protected: 
    int width, height, area; 
    public: 
    virtual void set_area() {} 
    }; 

class CS: public CC { 
    public: 
    void set_area() { area = width * height; } 
    }; 

class VCS: public VCC { 
    public: 
    void set_area() { area = width * height; } 
    }; 

void profileNonVirtual() { 
    CS *abc = new CS; 
    abc->set_area(); 
    delete abc; 
} 

void profileVirtual() { 
    VCS *abc = new VCS; 
    abc->set_area(); 
    delete abc; 
} 

int main() { 
    int iterations = 5000; 
    CProfiler prf2(&profileNonVirtual, iterations); 
    CProfiler prf(&profileVirtual, iterations); 

    std::cout << prf.result; 
    std::cout << "\n"; 
    std::cout << prf2.result; 

    return 0; 
} 

Al principio sólo hicieron 100 y 10000 iteraciones, y los resultados se preocupaban: 4 ms para no virtualizado, y 250m s para el virtualizado! Estuve casi "nooooooo" dentro, pero luego aumenté las iteraciones a alrededor de 500,000; para ver los resultados se vuelven casi completamente idénticos (tal vez un 5% más lentos sin indicadores de optimización habilitados).

Mi pregunta es, ¿por qué hubo un cambio tan significativo con una baja cantidad de iteraciones en comparación con una gran cantidad? ¿Fue simplemente porque las funciones virtuales están calientes en el caché en esas muchas iteraciones?

Negación
entiendo que mi código 'perfiles' no es perfecto, pero, como lo ha hecho, da una estimación de las cosas, que es lo único que importa en este nivel. También estoy haciendo estas preguntas para aprender, no para optimizar únicamente mi aplicación.

+0

plataforma y compilador? – ThomasMcLeod

+2

"(quizás un 5% más lento sin indicadores de optimización habilitados)" - esto implica que está creando un perfil de depuración/compilación no optimizada. Hacerlo arrojará un punto de referencia que generalmente está muy defectuoso para ser útil. ¿Es este el caso? –

+0

TI no funcionaba en Ubuntu 10.10, utilizando g ++, con _and_ sin indicadores de optimización. – dcousens

Respuesta

5

Extendiendo Charles' answer.

El problema aquí es que el bucle está haciendo algo más que probar la llamada virtual en sí (la asignación de memoria, probablemente, empequeñece la sobrecarga de llamadas virtuales de todos modos), por lo que su sugerencia es cambiar el código para que se prueba solamente la llamada virtual .

Aquí la función de punto de referencia es la plantilla, porque la plantilla puede estar en línea mientras que los punteros a través de la función de llamada son poco probables.

template <typename Type> 
double benchmark(Type const& t, size_t iterations) 
{ 
    timeval a, b; 
    gettimeofday(&a, 0); 
    for (;iterations > 0; --iterations) { 
    t.getArea(); 
    } 
    gettimeofday(&b, 0); 
    return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) - 
     (a.tv_sec * (unsigned int)1e6 + a.tv_usec); 
} 

Clases:

struct Regular 
{ 
    Regular(size_t w, size_t h): _width(w), _height(h) {} 

    size_t getArea() const; 

    size_t _width; 
    size_t _height; 
}; 

// The following line in another translation unit 
// to avoid inlining 
size_t Regular::getArea() const { return _width * _height; } 

struct Base 
{ 
    Base(size_t w, size_t h): _width(w), _height(h) {} 

    virtual size_t getArea() const = 0; 

    size_t _width; 
    size_t _height; 
}; 

struct Derived: Base 
{ 
    Derived(size_t w, size_t h): Base(w, h) {} 

    virtual size_t getArea() const; 
}; 

// The following two functions in another translation unit 
// to avoid inlining 
size_t Derived::getArea() const { return _width * _height; } 

std::auto_ptr<Base> generateDerived() 
{ 
    return std::auto_ptr<Base>(new Derived(3,7)); 
} 

Y la medición:

int main(int argc, char* argv[]) 
{ 
    if (argc != 2) { 
    std::cerr << "Usage: %prog iterations\n"; 
    return 1; 
    } 

    Regular regular(3, 7); 
    std::auto_ptr<Base> derived = generateDerived(); 

    double regTime = benchmark<Regular>(regular, atoi(argv[1])); 
    double derTime = benchmark<Base>(*derived, atoi(argv[1])); 

    std::cout << "Regular: " << regTime << "\nDerived: " << derTime << "\n"; 

    return 0; 
} 

Nota: esta prueba la sobrecarga de una llamada virtual en comparación con una función regular. La funcionalidad es diferente (ya que no tiene despacho en tiempo de ejecución en el segundo caso), pero por lo tanto es una sobrecarga en el peor de los casos.

EDITAR:

Resultados de la carrera (gcc.3.4.2, O2, servidor SLES10 quadcore) nota: con las definiciones de funciones en otra unidad de traducción, para prevenir inlining

> ./test 5000000 
Regular: 17041 
Derived: 17194 

No es realmente convincente.

+1

Agradezco la respuesta; pero los resultados no tienen sentido, y tardó 5 minutos simplemente para eliminar los errores de sintaxis. Su código (reparado) aquí http://www.pastie.org/1520895, avíseme si los cambios necesarios se estaban rompiendo o no. Los resultados fueron Regulares: 1 Derivados: 12548 para 5,000,000 iteraciones. – dcousens

+0

@Daniel: He respaldado las 3 soluciones, gracias. Tus resultados parecen extraños (por decir lo menos), veré si puedo encontrar algo de tiempo para ejecutar esto. –

+1

@Daniel: me he tomado el tiempo para compilar y ejecutar el código. Cuidando, como lo había observado en los comentarios, poner las definiciones de las funciones en otro archivo .cpp. He publicado los resultados, y como pueden ver ... no hay mucha diferencia, en absoluto. La explicación es probable que el almacenamiento en caché funcione en mi máquina. –

3

Con un pequeño número de iteraciones existe la posibilidad de que su código sea reemplazado por algún otro programa que se ejecute en paralelo o se produzca un intercambio o cualquier otro sistema operativo aísle el programa y tendrá el tiempo suspendido por el sistema operativo incluido en sus resultados de referencia. Esta es la razón número uno por la que debe ejecutar su código algo así como una docena de millones de veces para medir algo más o menos confiablemente.

+1

Sin embargo, esos resultados fueron consistentes, No lo intenté exactamente 'una vez'. – dcousens

+0

@Daniel: Puede ser cualquier cosa que no controle. Tratar de analizarlo de manera significativa es una pérdida de tiempo. Estarás mucho mejor si lo tratas como ruido. – sharptooth

2

Creo que este tipo de prueba es bastante inútil, de hecho:
1) está perdiendo el tiempo para perfilarse a sí mismo invocando gettimeofday();
2) no está realmente probando funciones virtuales, y en mi humilde opinión esto es lo peor.

¿Por qué? Debido a que el uso de funciones virtuales para evitar escribir cosas como:

<pseudocode> 
switch typeof(object) { 

case ClassA: functionA(object); 

case ClassB: functionB(object); 

case ClassC: functionC(object); 
} 
</pseudocode> 

en este código, se le pasa el bloque "if ... else" por lo que realmente no consigue la ventaja de las funciones virtuales. Este es un escenario donde siempre son "perdedores" contra los no virtuales.

Para hacer un perfil adecuado, creo que debe agregar algo como el código que he publicado.

11

Creo que su caso de prueba es demasiado artificial para ser de gran valor.

En primer lugar, dentro de su función de perfil asigna y desasigna dinámicamente un objeto, así como también llama a una función, si desea crear un perfil solo de la llamada a la función, debe hacerlo.

En segundo lugar, no está perfilando un caso en el que una llamada de función virtual representa una alternativa viable a un problema determinado. Una llamada de función virtual proporciona un despacho dinámico. Debe intentar perfilar un caso como, por ejemplo, donde se usa una llamada de función virtual como alternativa a algo utilizando un patrón de activación de tipo anti-patrón.

+0

¿Entonces está diciendo que debería intentar las mismas pruebas en una estructura de herencia más compleja (virtualizada)? También debe tenerse en cuenta que esta no es mi aplicación real, es puramente una prueba de funcionalidad. No tengo aversión a probar diferentes ejemplos. – dcousens

+1

+1: Nota. Solo quiero poner más énfasis en la parte de la llamada. Ni siquiera pongas el objeto en la pila ya que también estás midiendo el costo de la construcción del objeto cada vez (créalo una vez). –

+0

+1: (Si pudiera). Comparar manzanas y naranjas no es útil.Compare el despacho virtual con la alternativa (haciendo una llamada en función del tipo de objeto decidido en tiempo de ejecución). –

1

Cuando se utilizan muy pocas iteraciones, hay mucho ruido en la medición. La función gettimeofday no va a ser lo suficientemente precisa para darle buenas mediciones de sólo un puñado de iteraciones, por no hablar de que se registra el tiempo total de la pared (que incluye el tiempo cuando apropiado por otros hilos).

En pocas palabras, no se debe pensar en un diseño ridículamente enrevesado para evitar las funciones virtuales. Realmente no agregan mucha sobrecarga. Si tiene un código de rendimiento increíblemente crítico y sabe que las funciones virtuales constituyen la mayor parte del tiempo, entonces quizás sea algo de lo que preocuparse. En cualquier aplicación práctica, sin embargo, las funciones virtuales no serán lo que hace que su aplicación sea lenta.

+0

Gracias por una respuesta modesta, recuerde que como dije, esto no fue para optimizar mi aplicación, era puramente de interés, solo me preocupan los problemas de optimización del nivel de aplicación cuando uso Valgrind y Gprof los señala. – dcousens

2

Podría haber varias razones para la diferencia en el tiempo.

  • su función de temporización no es lo suficientemente precisa
  • el administrador del montón puede influir en el resultado, porque sizeof(VCS) > sizeof(VS). ¿Qué ocurre si se mueve la new/delete fuera del circuito?

  • Nuevamente, debido a las diferencias de tamaño, la memoria caché puede ser parte de la diferencia de tiempo.

PERO: en realidad se debe comparar una funcionalidad similar. Cuando utiliza funciones virtuales, lo hace por un motivo, que es llamar a una función de miembro diferente que depende de la identidad del objeto. Si necesita esta funcionalidad, y no desea utilizar las funciones virtuales, que tendría que poner en práctica de forma manual, ya sea utilizando una tabla de funciones o incluso una sentencia switch. Esto también tiene un costo, y eso es lo que debes comparar con las funciones virtuales.

+0

Usé el nuevo test de funcionalidad, los resultados fueron similares al comparar solo las funciones o solo la asignación. Es decir, más de 5.000.000 de iteraciones; en promedio, las pruebas virtualizadas fueron un 9% más lentas (se probaron 10 veces y se promediaron, por lo que realmente fueron 50,000,000). – dcousens

+0

Sí, pero no está asignando la misma cantidad de memoria. Entonces, cualquier cosa puede suceder debido al administrador de montón. –

+0

¿Cómo estoy asignando memoria? La declaración estaba en la pila, era puramente la función de llamadas ... (note mi fraseología: "los resultados fueron similares al comparar solo las funciones o solo la asignación"). – dcousens

2

Existe un impacto en el rendimiento al llamar a una función virtual, ya que hace un poco más que llamar a una función normal. Sin embargo, es probable que el impacto sea completamente insignificante en una aplicación en el mundo real, incluso menos de lo que aparece incluso en los puntos de referencia más finamente diseñados. En una aplicación del mundo real, la alternativa a una función virtual suele implicar la escritura manual de algún sistema similar, porque el comportamiento de llamar a una función virtual y llamar a una función no virtual es diferente. cambios basados ​​en el tipo de tiempo de ejecución del objeto invocado. Su punto de referencia, incluso sin tener en cuenta los defectos que tiene, no mide el comportamiento equivalente, solo la sintaxis equivalente-ish. Si se va a instituir una política que prohíbe la codificación de las funciones virtuales que le sea tiene que escribir algo de código potencialmente muy indirecta o confuso (que podría ser más lento) o volver a aplicar el mismo tipo de sistema de despacho de ejecución que el compilador está utilizando para implementar virtuales comportamiento de la función (que ciertamente no será más rápido que lo que hace el compilador, en la mayoría de los casos).

+0

Gracias por la respuesta, pero la pregunta no era si había un costo, sino por qué la diferencia en bajas iteraciones vs alta. Una * diferencia * consistente. – dcousens

0

En mi opinión, cuando había menos bucles, puede que no haya cambio de contexto, pero cuando aumenta el número de bucles, entonces hay grandes posibilidades de que se produzca el cambio de contexto y que domine la lectura . Por ejemplo, el primer programa tarda 1 segundo y el segundo programa 3 segundos, pero si el cambio de contexto dura 10 segundos, entonces la diferencia es 13/11 en lugar de 3/1.

Cuestiones relacionadas