Pregunta

Tener al menos un método virtual en una clase de C ++ (o cualquiera de sus clases padre) significa que la clase tendrá una tabla virtual, y cada instancia tendrá un puntero virtual.

Así que el coste de memoria es bastante clara. El más importante es el coste de memoria en los casos (especialmente si las instancias son pequeñas, por ejemplo, si sólo están destinados a contener un número entero:. En este caso tiene un puntero virtual en cada caso podría duplicar el tamaño de las instancias cuanto a el espacio de memoria utilizado por las mesas virtuales, supongo que por lo general es insignificante en comparación con el espacio utilizado por el código del método real.

Esto me lleva a mi pregunta: ¿hay un costo de rendimiento medible (es decir, la velocidad de impacto) para la fabricación de un método virtual? Habrá una búsqueda en la tabla virtual en tiempo de ejecución, sobre cada llamada a un método, por lo que si hay llamadas muy frecuentes a este método, y si este método es muy corto, es posible que haya un impacto en el rendimiento medible? Supongo que depende de la plataforma, pero nadie ha dirigido algunos puntos de referencia?

La razón por la que estoy pidiendo es que me encontré con un error que ha pasado a ser debido a un programador de olvidar para definir un método virtual. Esta no es la primera vez que veo este tipo de error. Y pensé: ¿por qué Añadir la palabra reservada virtual cuando sea necesario en lugar de retirar la palabra clave virtual cuando estamos absolutamente seguros de que es no necesita ? Si el costo de rendimiento es bajo, creo que simplemente voy a recomendar lo siguiente en mi equipo: simplemente hacer todos método virtual por defecto, incluyendo el destructor, de todas las clases, y sólo quitarlo cuando es necesario . ¿Le suena loco para usted?

¿Fue útil?

Solución

corrieron algunos tiempos en un 3GHz in- ordenar procesador PowerPC. En esa arquitectura, una llamada de función virtual cuesta 7 nanosegundos más de un (no virtual) a función directa.

Por lo tanto, no merece la pena preocuparse por el costo menos que la función es algo así como un trivial Get () / Set () de acceso, en la que nada que no sea en línea es una especie de desperdicio. Una sobrecarga 7ns en una función que inlines a 0.5ns es grave; una sobrecarga 7ns en una función que toma 500 ms para ejecutar carece de sentido.

La gran coste de las funciones virtuales no es realmente la búsqueda de un puntero de función en la viable (que es por lo general sólo un solo ciclo), pero que el salto indirecto por lo general no puede ser predicha-rama. Esto puede causar una gran burbuja de tuberías como el procesador no puede recuperar cualquier instrucción hasta que el salto indirecto (la llamada a través del puntero de función) se ha retirado y un nuevo indicador de instrucción computarizada. Por lo tanto, el costo de una llamada de función virtual es mucho más grande de lo que parece de mirar a la asamblea ... pero aún así sólo 7 nanosegundos.

Editar Andrew, No estoy seguro, y otros también elevan el muy buen punto de que una llamada de función virtual puede causar un fallo de caché de instrucciones: si usted salta a una dirección de código que no está en la memoria caché y luego todo el programa llega a un punto muerto, mientras que las instrucciones que se obtienen de la memoria principal. Este es siempre un puesto importante: el xenón, unos 650 ciclos (por mis pruebas).

Sin embargo, esto no es un problema específico de las funciones virtuales ya que incluso una llamada de función directa provocará un fallo si saltas a las instrucciones que no están en la memoria caché. Lo que importa es si la función se ha ejecutado antes recientemente (lo que es más probable que sea en caché), y si su arquitectura puede predecir ramas estáticos (no virtuales) a buscar a esas instrucciones en la caché antes de tiempo. Mi PPC no es así, pero tal vez el hardware más reciente de Intel hace.

Mis tiempos de controlar la influencia de fallos en la ejecución iCache (deliberadamente, ya que yo estaba tratando de examinar la tubería de la CPU en forma aislada), por lo que descartan que el costo.

Otros consejos

No hay duda de arriba medible cuando se llama a una función virtual - la llamada debe utilizar la viable para resolver la dirección de la función para ese tipo de objeto. Las instrucciones adicionales son el menor de sus preocupaciones. No sólo vtables prevenir posibles muchas optimizaciones del compilador (ya que el tipo es polimórfico el compilador) también pueden thrash su I-caché.

Por supuesto, si estas penas son significativas o no depende de su aplicación, con qué frecuencia se ejecutan las rutas de código, y sus patrones de herencia.

En mi opinión, sin embargo, tener todo como virtual por defecto es una solución general a un problema que podría resolver de otras maneras.

Tal vez usted podría mirar cómo se diseñan las clases / documentado / escrito. En general, la cabecera para una clase debe hacer bastante claro qué funciones pueden ser anulados por las clases derivadas y cómo se llaman. Que tienen los programadores escriben esta documentación es útil para asegurar que están marcados correctamente como virtual.

Me gustaría también decir que declarar cada función como virtual podría dar lugar a más errores que simplemente olvidarse de marcar algo como virtual. Si todas las funciones son todo lo virtual puede ser reemplazado por clases base - pública, protegido, privado - todo se convierte en un juego justo. Por accidente o intención subclases podría entonces cambiar el comportamiento de las funciones que a continuación causan problemas cuando se utiliza en la implementación base.

Depende. :) (Si hubiera esperado otra cosa?)

Una vez que una clase tiene una función virtual, ya no puede ser un tipo de datos POD, (no puede haber sido uno antes de que cualquiera, en cuyo caso esto no va a hacer la diferencia) y que hace que toda una serie de optimizaciones imposibles .

std :: copy () sobre los tipos de civil POD puede recurrir a una simple rutina de establecimiento de memoria, pero los tipos no-POD que ser manejado con más cuidado.

La construcción se convierte en mucho más lento debido a la viable tiene que ser inicializado. En el peor de los casos, la diferencia de rendimiento entre la vaina y tipos de datos no-POD puede ser significativo.

En el peor de los casos, es posible que vea 5x ejecución más lenta (ese número se ha tomado de un proyecto universitario que hice recientemente volver a implementar algunas clases de la biblioteca estándar. Nuestro contenedor tomó aproximadamente 5 veces el tiempo para construir tan pronto como el tipo de datos que almacenado tiene un vtable)

Por supuesto, en la mayoría de los casos, es poco probable que haya una diferencia de rendimiento medible, esto es simplemente para señalar que en casos fronterizos, puede ser costoso.

Sin embargo, el rendimiento no debería ser su principal consideración aquí. Haciendo todo lo virtual no es una solución perfecta por otras razones.

Permitir que todo lo que se reemplaza en las clases derivadas hace que sea mucho más difícil de mantener invariantes de clase. ¿Cómo funciona una garantía de la clase que se mantiene en un estado coherente cuando cualquiera de sus métodos podría redefinirse en cualquier momento?

Hacer que todo virtual puede eliminar algunos errores potenciales, sino que también introduce otros nuevos.

Si necesita la funcionalidad de despacho virtual, usted tiene que pagar el precio. La ventaja de C ++ es que se puede utilizar una implementación muy eficiente de despacho virtual proporcionado por el compilador, en lugar de una versión posiblemente ineficiente implementar mismo.

Sin embargo, explotación de árboles usted mismo con la cabeza si no se needx posiblemente va un poco demasiado lejos. Y la mayoría no classesare diseñados para ser heredado de -. Para crear una buena clase base requiere algo más que hacer sus funciones virtuales

despacho virtual es un orden de magnitud más lento que algunas alternativas - no debido a indirección tanto como la prevención de procesos en línea. A continuación, ilustro que mediante el contraste de despacho virtual con una implementación de la incorporación de una "(Cómo Identificar) número de tipo" en los objetos y el uso de una sentencia switch para seleccionar el código de tipo específico. Esto evita la sobrecarga de llamada a la función por completo - sólo hacer un salto local. Hay un coste potencial de mantenimiento, dependencias de recompilación etc través de la localización forzada (en el interruptor) de la funcionalidad de tipo específico.


APLICACIÓN

#include <iostream>
#include <vector>

// virtual dispatch model...

struct Base
{
    virtual int f() const { return 1; }
};

struct Derived : Base
{
    virtual int f() const { return 2; }
};

// alternative: member variable encodes runtime type...

struct Type
{
    Type(int type) : type_(type) { }
    int type_;
};

struct A : Type
{
    A() : Type(1) { }
    int f() const { return 1; }
};

struct B : Type
{
    B() : Type(2) { }
    int f() const { return 2; }
};

struct Timer
{
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
    struct timespec from;
    double elapsed() const
    {
        struct timespec to;
        clock_gettime(CLOCK_MONOTONIC, &to);
        return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
    }
};

int main(int argc)
{
  for (int j = 0; j < 3; ++j)
  {
    typedef std::vector<Base*> V;
    V v;

    for (int i = 0; i < 1000; ++i)
        v.push_back(i % 2 ? new Base : (Base*)new Derived);

    int total = 0;

    Timer tv;

    for (int i = 0; i < 100000; ++i)
        for (V::const_iterator i = v.begin(); i != v.end(); ++i)
            total += (*i)->f();

    double tve = tv.elapsed();

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';

    // ----------------------------

    typedef std::vector<Type*> W;
    W w;

    for (int i = 0; i < 1000; ++i)
        w.push_back(i % 2 ? (Type*)new A : (Type*)new B);

    total = 0;

    Timer tw;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
        {
            if ((*i)->type_ == 1)
                total += ((A*)(*i))->f();
            else
                total += ((B*)(*i))->f();
        }

    double twe = tw.elapsed();

    std::cout << "switched: " << total << ' ' << twe << '\n';

    // ----------------------------

    total = 0;

    Timer tw2;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
            total += (*i)->type_;

    double tw2e = tw2.elapsed();

    std::cout << "overheads: " << total << ' ' << tw2e << '\n';
  }
}

Resultados Rendimiento

En mi sistema Linux:

~/dev  g++ -O2 -o vdt vdt.cc -lrt
~/dev  ./vdt                     
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726

Esto sugiere un enfoque de conmutación de tipo-número en línea es de aproximadamente (1,28 - 0,23) / (0,344 a 0,23) = 9.2 veces más rápido. Por supuesto, eso es específico para el sistema exacto banderas / compilador y versión, etc., pero generalmente indicativa probado.


COMENTARIOS RE DISPATCH VIRTUAL

Hay que decir sin embargo que los gastos generales de llamada de función virtual son algo que rara vez es significativa, y sólo por algunas veces denominadas funciones triviales (como captadores y definidores). Incluso entonces, usted podría ser capaz de proporcionar una única función para obtener y establecer un montón de cosas a la vez, lo que minimiza el costo. La gente se preocupa sobre manera despacho virtual demasiado - por lo que hacen los perfiles antes de encontrar alternativas incómodas. El principal problema con ellos es que realizan una llamada a una función fuera de línea, aunque también deslocalizan el código ejecutado que cambia los patrones de utilización de la memoria caché (para bien o (más a menudo) peor).

El costo adicional es prácticamente nada en la mayoría de los escenarios. (Perdón por el juego de palabras). Ejac ya ha publicado las medidas relativas sensibles.

Lo más importante es que renunciar a posibles optimizaciones debido a los procesos en línea. Pueden ser especialmente bueno si la función es llamada con parámetros constantes. Esto rara vez hace una diferencia real, pero en algunos casos, esto puede ser enorme.


En cuanto a las optimizaciones:
Es importante conocer y tener en cuenta el costo relativo de las construcciones de la lengua. notación O grande es la mitad de la historia ONL - ¿Cómo funciona su escala de aplicación . La otra mitad es el factor constante en frente de ella.

Como regla general, yo no saldría de mi camino para evitar las funciones virtuales, a menos que haya indicios claros y específicos que se trata de un cuello de botella. Un diseño limpio siempre es lo primero - pero es sólo una de las partes interesadas que no deberían indebidamente herir a los demás.


Contrived Ejemplo: Un destructor virtual vacío en una matriz de un millón de elementos pequeños puede arar a través de al menos 4 MB de datos, golear su caché. Si ese destructor puede ser inline de distancia, no se tocarán los datos.

Al escribir código de la biblioteca, tales consideraciones están lejos de ser prematuro. Nunca se sabe cuántos bucles se puso alrededor de su función.

Mientras todos los demás es correcta sobre el funcionamiento de los métodos virtuales y tal, creo que el verdadero problema es si el equipo sabe acerca de la definición de la palabra clave virtual en C ++.

Considere este código, ¿cuál es la salida?

#include <stdio.h>

class A
{
public:
    void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

No hay nada sorprendente aquí:

A::Foo()
B::Foo()
A::Foo()

Como nada es virtual. Si se añade la palabra clave virtual al frente de Foo en ambas clases A y B, obtenemos esto para la salida:

A::Foo()
B::Foo()
B::Foo()

Más o menos lo que todos esperan.

Ahora, usted ha mencionado que hay errores porque alguien se olvidó de agregar una palabra clave virtual. Así que considera este código (en la que se añade la palabra clave virtual a una, pero no la clase B). ¿Cuál es la salida entonces?

#include <stdio.h>

class A
{
public:
    virtual void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Respuesta: El mismo que si se añade la palabra clave virtual de A a B? La razón es que la firma de B :: Foo coincide exactamente como A :: foo () y porque de una Foo es virtual, por lo que es B.

Ahora consideremos el caso en que B es Foo virtual y una de no lo es. ¿Cuál es la salida entonces? En este caso, la salida es

<*>

La palabra clave virtual funciona hacia abajo en la jerarquía, no hacia arriba. Nunca hace que los métodos de la clase base virtual. La primera vez que se encuentra un método virtual en la jerarquía es cuando comienza el polimorfismo. No hay un camino para las clases más tarde para hacer las clases anteriores tienen métodos virtuales.

No hay que olvidar que los métodos virtuales hacen que esta clase está dando clases futuras la capacidad de anular / cambiar algunos de sus comportamientos.

Así que si usted tiene una regla para eliminar la palabra clave virtual, no puede tener el efecto deseado.

La palabra clave virtual en C ++ es un concepto poderoso. Usted debe asegurarse de que cada miembro del equipo sabe realmente este concepto para que pueda ser utilizado como fue diseñado.

Dependiendo de la plataforma, la sobrecarga de una llamada virtual puede ser muy indeseable. Al declarar cada función virtual básicamente estás llamando a todos ellos a través de un puntero de función. Por lo menos este es un desreferenciar extra, pero en algunas plataformas PPC usará microcodificado o no instrucciones lentas para lograr esto.

Me gustaría recomendar en contra de su propuesta por este motivo, pero si le ayuda a evitar que los insectos, entonces puede ser vale la pena el comercio fuera. No puedo evitar pensar que debe haber un punto medio que vale la pena encontrar, sin embargo.

Se requerirá sólo un par de instrucción asm extra para llamar al método virtual.

Pero no creo que le preocupa que la diversión (int a, int b) tiene un par de instrucciones adicionales de 'empuje' en comparación con la diversión (). Así que no se preocupe por los virtuales también, hasta que esté en situación especial y ver que lo que realmente conduce a problemas.

P.S. Si usted tiene un método virtual, asegúrese de que tiene un destructor virtual. De esta manera evitará posibles problemas


En respuesta a 'xtofl' y los comentarios 'Tom'. Hice pruebas pequeñas con 3 funciones:

  1. virtual
  2. Normal
  3. Normal con parámetros 3 int

Mi prueba fue una iteración simple:

for(int it = 0; it < 100000000; it ++) {
    test.Method();
}

Y aquí los resultados:

  1. 3913 sec
  2. 3873 sec
  3. 3970 sec

Fue compilado por VC ++ en modo de depuración. Hice sólo 5 ensayos por método y se calcula el valor medio (lo que los resultados pueden ser bastante inexacta) ... De cualquier forma, los valores son casi iguales asumir 100 millones de llamadas. Y el método con el empuje adicional 3 / pop fue más lento.

El punto principal es que si no te gusta la analogía con el empuje / pop, piensa extra si / persona en su código? ¿Cree usted acerca de la tubería de la CPU cuando se agrega extra si / else ;-) Además, nunca se sabe en lo que el código de la CPU va a correr ... compilador habituales pueden genera código más óptimo para una CPU y menos óptimo para otro (< a href = "http://en.wikipedia.org/wiki/Intel_C%2B%2B_Compiler" rel = "nofollow noreferrer"> Intel C ++ Compiler )

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