2012-03-31 21 views
7

Escribí un ejemplo simple, que estima el tiempo promedio de llamada de la función virtual, utilizando la interfaz de clase base y dynamic_cast y llamada de la función no virtual. aquí es que:¿Por qué la llamada a función virtual es más rápida que dynamic_cast?

#include <iostream> 
#include <numeric> 
#include <list> 
#include <time.h> 

#define CALL_COUNTER (3000) 

__forceinline int someFunction() 
{ 
    return 5; 
} 

struct Base 
{ 
    virtual int virtualCall() = 0; 
    virtual ~Base(){}; 
}; 

struct Derived : public Base 
{ 
    Derived(){}; 
    virtual ~Derived(){}; 
    virtual int virtualCall(){ return someFunction(); }; 
    int notVirtualCall(){ return someFunction(); }; 
}; 


struct Derived2 : public Base 
{ 
    Derived2(){}; 
    virtual ~Derived2(){}; 
    virtual int virtualCall(){ return someFunction(); }; 
    int notVirtualCall(){ return someFunction(); }; 
}; 

typedef std::list<double> Timings; 

Base* createObject(int i) 
{ 
    if(i % 2 > 0) 
    return new Derived(); 
    else 
    return new Derived2(); 
} 

void callDynamiccast(Timings& stat) 
{ 
    for(unsigned i = 0; i < CALL_COUNTER; ++i) 
    { 
    Base* ptr = createObject(i); 

    clock_t startTime = clock(); 

    for(int j = 0; j < CALL_COUNTER; ++j) 
    { 
     Derived* x = (dynamic_cast<Derived*>(ptr)); 
     if(x) x->notVirtualCall(); 
    } 

    clock_t endTime = clock(); 
    double callTime = (double)(endTime - startTime)/CLOCKS_PER_SEC; 
    stat.push_back(callTime); 

    delete ptr; 
    } 
} 

void callVirtual(Timings& stat) 
{ 
    for(unsigned i = 0; i < CALL_COUNTER; ++i) 
    { 
    Base* ptr = createObject(i); 

    clock_t startTime = clock(); 

    for(int j = 0; j < CALL_COUNTER; ++j) 
     ptr->virtualCall(); 


    clock_t endTime = clock(); 
    double callTime = (double)(endTime - startTime)/CLOCKS_PER_SEC; 
    stat.push_back(callTime); 

    delete ptr; 
    } 
} 

int main() 
{ 
    double averageTime = 0; 
    Timings timings; 


    timings.clear(); 
    callDynamiccast(timings); 
    averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0); 
    averageTime /= timings.size(); 
    std::cout << "time for callDynamiccast: " << averageTime << std::endl; 

    timings.clear(); 
    callVirtual(timings); 
    averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0); 
    averageTime /= timings.size(); 
    std::cout << "time for callVirtual: " << averageTime << std::endl; 

    return 0; 
} 

Parece que callDynamiccast lleva casi dos veces más.

time for callDynamiccast: 0.000240333

time for callVirtual: 0.0001401

Cualquier idea por qué lo hace?

EDITADO: la creación de objetos se realiza ahora en la función separada, por lo que el complemento no la conoce como tipo real. Casi el mismo resultado.

EDITED2: crea dos tipos diferentes de objetos derivados.

+1

Probablemente necesite ejecutar muchas más iteraciones para obtener una medida estadística decente. ¿Está compilando en la configuración de optimización más alta? –

+0

Lo intenté con muchas más iteraciones, pero el resultado es el mismo. Todas las optimizaciones están desactivadas. Usé MSVS2008. –

+4

Su prueba no es válida, porque el compilador puede optimizar fácilmente tanto la llamada virtual (en una llamada no virtual) como la dinámica_cast (básicamente en un noop), porque sabe que 'ptr' realmente apunta a un objeto' Derived'. –

Respuesta

11

La llamada a la función virtual es similar a un puntero a la función, o si el compilador conoce el tipo, el envío estático. Este es un tiempo constante.

dynamic_cast es bastante diferente: utiliza un medio definido por la implementación para determinar un tipo. No es un tiempo constante, puede atravesar la jerarquía de clases (también considerar la herencia múltiple) y realizar varias búsquedas. Una implementación puede usar comparaciones de cadenas. Por lo tanto, la complejidad es mayor en dos dimensiones. Los sistemas en tiempo real a menudo evitan/desalientan dynamic_cast por estos motivos.

Más detalles están disponibles in this document.

+0

Entonces, si amplío la jerarquía de clases, el tiempo de dynamic_cast aumentará aún más? –

+4

@Dmitry cómo se implementa está definido por su implementación, pero como una generalización sí, eso es correcto. la complejidad de la herencia (número de bases y si se usa herencia múltiple) es generalmente donde se introduce el costo. si las clases no están relacionadas, su implementación puede tener una buena optimización para ese caso. También tenga en cuenta que hay algunos casos extremos que aumentan la complejidad: 'dynamic_cast' puede fallar cuando no se puede determinar una base singular porque existen dos bases comunes. por lo tanto, toda la jerarquía debe ser examinada antes de regresar. – justin

+0

Página 31 del documento vinculado tiene los detalles pertinentes acerca de una llamada virtual frente a varios dynamic_cast para varios compiladores (aunque no pude encontrar dónde le dijo qué compiladores se utilizaron). – paxos1977

3

Usted solo está midiendo el costo de dynamic_cast<>. Se implementa con RTTI, que es opcional en cualquier compilador de C++. Project + Properties, C/C++, Language, Enable Run-Time Type Info setting. Cámbielo a No.

Ahora recibirá un recordatorio poco sutil de que dynamic_cast<> ya no puede realizar el trabajo adecuado. Arregla arbitrariamente en static_cast<> para obtener resultados drásticamente diferentes. El punto clave aquí es que si sabe que un upcast es siempre seguro, entonces static_cast<> le compra el rendimiento que está buscando. Si no sabe por el hecho de que la actualización es segura, entonces dynamic_cast<> lo mantiene alejado de problemas. Es el tipo de problema que es terriblemente difícil de diagnosticar. El modo de falla común es la corrupción del montón, solo obtendrá un GPF inmediato si tiene mucha suerte.

4

Cabe señalar que propósito completo de funciones virtuales es no tener que arrojar el gráfico de herencia. Existen funciones virtuales para que pueda usar una instancia de clase derivada como si fuera una clase base. Para que se puedan llamar implementaciones más especializadas de funciones desde un código que originalmente se llamaba versiones de clase base.

Si las funciones virtuales fueran más lentas que un lanzamiento seguro a la llamada de función de clase derivada +, entonces los compiladores de C++ simplemente implementarían llamadas a funciones virtuales de esa manera.

Así que no hay ninguna razón para esperar que dynamic_cast + llame para ser más rápido.

0

La diferencia es que puede llamar a la función virtual en cualquier instancia derivada de Base.El miembro notVirtualCall() no existe dentro de Base, y no se puede llamar sin primero determinar el tipo dinámico exacto del objeto.

La consecuencia de esta diferencia es que el vtable de la clase base incluye una ranura para virtualCall(), que contiene un puntero a la función correcta para llamar. Entonces, la llamada virtual simplemente persigue el puntero vtable incluido como el primer miembro (invisible) de todos los objetos de tipo Base, carga el puntero desde la ranura correspondiente a virtualCall() y llama a la función detrás de ese puntero.

Cuando hace un dynamic_cast<>, por el contrario, la clase Base no sabe en tiempo de compilación qué otras clases eventualmente derivarán de ella. En consecuencia, no puede incluir información dentro de su vtable que facilite la resolución del dynamic_cast<>. Esa es la falta de información que hace que dynamic_cast<> sea más costoso de implementar que una llamada de función virtual. El dynamic_cast<> tiene que buscar realmente a través del árbol de herencia del objeto real para comprobar si el tipo de destino del molde se encuentra entre sus bases. Ese es un trabajo que la llamada virtual evita.

Cuestiones relacionadas