C++

2009-10-26 6 views
8

Considere:C++

class A 
{ 
    public: 
     virtual void update() = 0; 
} 

class B : public A 
{ 
    public: 
     void update() { /* stuff goes in here... */ } 

    private: 
     double a, b, c; 
} 

class C { 
    // Same kind of thing as B, but with different update function/data members 
} 

Ahora estoy haciendo:

A * array = new A[1000]; 
array[0] = new B(); 
array[1] = new C(); 
//etc., etc. 

si llamo sizeof(B), el tamaño devuelto es el tamaño requerido por los 3 miembros dobles, además de Se requieren algunos gastos generales para la tabla de punteros de funciones virtuales. Ahora, volviendo a mi código, resulta que 'sizeof (myclass)' es 32; es decir, estoy usando 24 bytes para mis miembros de datos y 8 bytes para la tabla de funciones virtuales (4 funciones virtuales). Mi pregunta es: ¿hay alguna manera de simplificar esto? Mi programa eventualmente usará muchísima memoria, y no me gusta el sonido del 25% de lo que consumen los punteros de funciones virtuales.

+3

No olvide destructores virtuales. – GManNickG

+0

En cuanto a su estrategia de asignación, es mejor que investigue utilizando un asignador personalizado de cada 'B',' C', etc. Usar un 'nuevo' básico para una gran cantidad de objetos individuales puede agregar más sobrecarga de lo que cree . –

+0

Como se indica en las respuestas, solo paga un puntero por instancia (4 bytes en x86 y 8 bytes en x64). Sin embargo, la alineación de memoria puede cambiar la imagen un poco (por ejemplo, si tiene una clase con un char y un método virtual, generalmente tomará 8 bytes en x86). Además, el compilador de Microsoft C++ tiene un error conocido al alinear el vfptr que puede agregar 4 o más bytes al tamaño de la clase. – sbk

Respuesta

10

Normalmente, cada instancia de una clase con al menos una función virtual tendrá un puntero adicional almacenado con sus miembros de datos explícitos.

No hay forma de evitar esto, pero recuerde que (una vez más) cada tabla de funciones virtuales se comparte entre todas las instancias de la clase, por lo que no hay gran sobrecarga para tener múltiples funciones virtuales o niveles extra de herencia una vez pagó el "impuesto vptr" (pequeño costo del puntero vtable).

Para clases más grandes, la sobrecarga se vuelve mucho más pequeña como un porcentaje.

Si desea una funcionalidad que haga algo como lo que hacen las funciones virtuales, tendrá que pagar de alguna manera. En realidad, el uso de funciones virtuales nativas puede ser la opción más barata.

38

La tabla v es por clase y no por objeto. Cada objeto contiene solo un puntero en su tabla v. Entonces la sobrecarga por instancia es sizeof(pointer) (generalmente 4 u 8 bytes). No importa cuántas funciones virtuales tenga para el tamaño del objeto de clase. Teniendo esto en cuenta, creo que no deberías preocuparte demasiado por eso.

+0

@ 280Z28: gracias por la edición ... – Ponting

+1

solo para agregarlo - todavía hay una tabla v por clase, que contiene al menos un puntero por función virtual. –

+1

Todavía paga el costo adicional por el - en muchos casos totalmente innecesario - vtable. Simplemente llena el espacio por clase en bits 'globales' del ejecutable, no por instancia. Dicho espacio puede ser altamente significativo, especialmente si se hereda cuando no es necesario, de clases con un alto número de funciones virtuales. En tales casos, cambiar a composición puede reducir drásticamente el binario. Reduje el binario de un proyecto en 1/6º al dejar de heredar innecesariamente del tipo de biblioteca de alguien. Además, tiene la ventaja habitual de liberar el diseño de clase de todas las otras limitaciones bien documentadas de la herencia –

6

Tiene dos opciones.

1) No se preocupe.

2) No utilice funciones virtuales. Sin embargo, no usar funciones virtuales puede simplemente mover el tamaño en su código, ya que su código se vuelve más complejo.

6

El costo de espacio de un vtable es un puntero (alineación de módulo). La tabla en sí misma no se coloca en cada instancia de la clase.

+0

Al colocar el puntero vtable al frente de la clase, no hay problemas de alineación (por lo que la mayoría de los compiladores lo colocan allí). –

+3

Los problemas de alineación siguen allí si para empezar, el tamaño de la clase era inferior al puntero vtable. Diga 'struct C {char c; } 'probablemente será del tamaño 1, y cualquier conjunto de tales los tendrá fuertemente empaquetados. Si agrega un miembro virtual, en una plataforma de 32 bits, es probable que rellene los 5 caracteres de almacenamiento resultantes en 8, por lo que ahora tiene 3 caracteres desperdiciados por objeto. –

+1

Esta respuesta menciona explícitamente "alineación", y de hecho este es un problema en los objetivos con poca RAM (microcontroladores). Es como lo describió Pavel, un char + el puntero vtable (4 bytes) da como resultado ocho bytes en un ARM Cortex-Mx de 32 bits (hay un relleno de tres bytes probablemente/con suerte antes del ptr vtable). – Beryllium

1

Si conoce todos los tipos derivados y sus respectivas funciones de actualización de antemano, puede almacenar el tipo derivado en A e implementar el envío manual para el método de actualización.

Sin embargo, como otros señalan, realmente no está pagando demasiado por el vtable, y la compensación es la complejidad del código (¡y dependiendo de la alineación, es posible que no esté guardando ningún tipo de memoria!). Además, si alguno de sus miembros de datos tiene un destructor, entonces también debe preocuparse por el envío manual del destructor.

Si todavía quiere ir a esta ruta, que podría tener este aspecto:

class A; 
void dispatch_update(A &); 

class A 
{ 
public: 
    A(char derived_type) 
     : m_derived_type(derived_type) 
    {} 
    void update() 
    { 
     dispatch_update(*this); 
    } 
    friend void dispatch_update(A &); 
private: 
    char m_derived_type; 
}; 

class B : public A 
{ 
public: 
    B() 
     : A('B') 
    {} 
    void update() { /* stuff goes in here... */ } 

private: 
    double a, b, c; 
}; 

void dispatch_update(A &a) 
{ 
    switch (a.m_derived_type) 
    { 
    case 'B': 
     static_cast<B &> (a).update(); 
     break; 
    // ... 
    } 
} 
2

cuántas instancias de clases A-derivada se puede esperar? ¿Cuántas clases distintas derivadas de A espera usted?

Tenga en cuenta que incluso con un millón de instancias, estamos hablando de un total de 32MB. Hasta 10 millones, no te preocupes.

Generalmente necesita un puntero adicional por instancia (si está ejecutando en una plataforma de 32 bits, los últimos 4 bytes se deben a la alineación). Cada clase consume (Number of virtual functions * sizeof(virtual function pointer) + fixed size) bytes adicionales para su VMT.

Tenga en cuenta que, teniendo en cuenta la alineación para los dobles, incluso un byte único como identificador de tipo elevará el tamaño del elemento a 32, por lo que la solución de Stjepan Rajko es útil en algunos casos, pero no en el suyo.

Además, no olvide la sobrecarga de un montón general para tantos objetos pequeños. Puede tener otros 8 bytes por objeto. Con un administrador de montón personalizado, como un objeto/tamaño específico pool allocator, puede guardar más aquí y emplear una solución estándar.

1

Está agregando un único puntero a un vtable para cada objeto; si agrega varias funciones virtuales nuevas, el tamaño de cada objeto no aumentará. Tenga en cuenta que incluso si se encuentra en una plataforma de 32 bits donde los punteros tienen 4 bytes, verá que el tamaño del objeto aumenta en 8 probablemente debido a los requisitos generales de alineación de la estructura (es decir, obtendrá 4 bytes de relleno).

De modo que, incluso si hiciera que la clase no fuera virtual, agregar un solo miembro de pila probablemente agregaría un total de 8 bytes al tamaño de cada objeto.

creo que las únicas formas en las que será capaz de reducir el tamaño de los objetos que serían los siguientes:

  • hacerlos no virtual (? Que lo que realmente necesita el comportamiento polimórfico)
  • uso flotantes en lugar de dobles para uno o más miembros de datos si no necesita la precisión
  • si es probable que vea muchos objetos con los mismos valores para los miembros de datos, es posible que pueda ahorrar espacio de memoria a cambio para cierta complejidad en la gestión de los objetos mediante el Flyweight design pattern
1

No responde directamente a la pregunta, pero también considera que el orden de declaración de los miembros de su información puede aumentar o disminuir el consumo real de memoria por objeto de clase. Esto se debe a que la mayoría de los compiladores no pueden (leer: no) optimizar el orden en que los miembros de la clase se disponen en la memoria para disminuir la fragmentación interna debido a problemas de alineación.

+0

Yo diría que no pueden hacerlo. C++ permite comparaciones de punteros entre direcciones de miembros de datos del mismo objeto (con algunas restricciones), requiriendo que los resultados de dichas comparaciones reflejen el orden de la declaración. Yo diría que cualquier tipo de "magia del compilador" necesaria para respaldar este requisito con los miembros de datos reorganizados resultaría demasiado costoso. – AnT

+0

Resumiría que dos punteros son comparables aritméticamente, pero el tipado de C++ a veces requiere que lo hagas. De cualquier manera, la "magia del compilador" que entraría en dicho sistema se calcularía para minimizar la fragmentación interna de los objetos de la clase como un todo, y no para cada instancia. Todo esto supone que no he entendido mal tu comentario. –

2

Si va a tener millones de estas cosas, y la memoria es una gran preocupación para usted, entonces probablemente no debería hacerles objetos. Simplemente declare como una estructura o una matriz de 3 dobles (o lo que sea), y ponga las funciones para manipular los datos en otro lugar.

Si realmente necesita el comportamiento polimórfico, es probable que no se puede ganar, ya que la información del tipo que tendría que almacenar en su estructura va a terminar teniendo una cantidad similar de espacio ...

Es es probable que tenga grandes grupos de objetos del mismo tipo? En ese caso, se puede poner la información de tipo de una sola planta "arriba" de la persona "A" clases ...

Algo así como:

class A_collection 
{ 
    public: 
     virtual void update() = 0; 
} 

class B_collection : public A_collection 
{ 
    public: 
     void update() { /* stuff goes in here... */ } 

    private: 
     vector<double[3]> points; 
} 

class C_collection { /* Same kind of thing as B_collection, but with different update function/data members */ 
0

Teniendo en cuenta todas las respuestas que ya están aquí, creo Debo estar loco, pero esto me parece correcto, así que lo estoy publicando de todos modos.Cuando vi por primera vez el ejemplo del código, pensé que estaba cortando las instancias de B y C, pero luego miré un poco más cerca. Ahora estoy razonablemente seguro de que su ejemplo no se compilará en absoluto, pero no tengo un compilador en esta casilla para probar.

A * array = new A[1000]; 
array[0] = new B(); 
array[1] = new C(); 

Para mí, esto se parece a la primera línea asigna un conjunto de 1.000 A. Las dos líneas siguientes operan en los elementos primero y segundo de esa matriz, respectivamente, que son instancias de A, no punteros a A. Por lo tanto, no puede asignar un puntero a A a esos elementos (y new B() devuelve dicho puntero). Los tipos no son los mismos, por lo tanto, debería fallar en el momento de la compilación (a menos que A tenga un operador de asignación que tome un A*, en cuyo caso hará lo que usted le indique que haga).

Entonces, ¿estoy completamente fuera de la base? Estoy ansioso por descubrir lo que me perdí.

5

alejando de la no emisión del puntero vtable en su objeto:

su código tiene otros problemas:

A * array = new A[1000]; 
array[0] = new B(); 
array[1] = new C(); 

El problema que tiene es el problema de corte.
No puede poner un objeto de clase B en un espacio del tamaño reservado para un objeto de clase A.
Simplemente cortará la parte B (o C) del objeto, dejándolo con solo la parte A.

Lo que quieres hacer. Tiene una matriz de punteros A para que contenga cada elemento con un puntero.

A** array = new A*[1000]; 
array[0] = new B(); 
array[1] = new C(); 

Ahora tiene otro problema de destrucción. De acuerdo. Esto podría continuar por años.
corto utilización respuesta impulso: ptr_vector <>

boost:ptr_vector<A> array(1000); 
array[0] = new B(); 
array[1] = new C(); 

Nunca allocte matriz de esa manera a menos que tenga a (Su demasiado Java, como para ser útil).

+4

Estrictamente hablando, la versión original ni siquiera compilará. Intenta asignar valores * pointer * a elementos de la matriz, cuando los elementos de la matriz no son punteros. Es demasiado pronto para afirmar que tiene un "problema de corte", hasta que el código se vuelva compilable. – AnT

+0

@AndretT: cierto. Saltó el arma. –

1

Como ya se ha dicho, en un enfoque típico de implementación popular, una vez que una clase se vuelve polimórfica, cada instancia aumenta en un tamaño de un puntero de datos ordinario. No importa cuántas funciones virtuales tenga en su clase. En una plataforma de 64 bits, el tamaño aumentaría en 8 bytes. Si observaba un crecimiento de 8 bytes en una plataforma de 32 bits, podría haber sido causado por el relleno añadido al puntero de 4 bytes para la alineación (si su clase tiene un requisito de alineación de 8 bytes).

Además, vale la pena señalar que herencia virtual puede inyectar punteros de datos adicionales en instancias de clase (punteros de base virtual). Solo estoy familiarizado con algunas implementaciones y en al menos uno el número de punteros base virtuales era el mismo que el número de bases virtuales en la clase, lo que significa que la herencia virtual puede agregar múltiples punteros de datos internos a cada instancia.

-1

Si realmente desea guardar la memoria del puntero de la tabla virtual en cada objeto entonces se puede aplicar el código en C-estilo ...

P. ej

struct Point2D { 
int x,y; 
}; 

struct Point3D { 
int x,y,z; 
}; 

void Draw2D(void *pThis) 
{ 
    Point2D *p = (Point2D *) pThis; 
    //do something 
} 

void Draw3D(void *pThis) 
{ 
    Point3D *p = (Point3D *) pThis; 
//do something 
} 

int main() 
{ 

    typedef void (*pDrawFunct[2])(void *); 

    pDrawFunct p; 
    Point2D pt2D; 
    Point3D pt3D; 

    p[0] = &Draw2D; 
    p[1] = &Draw3D;  

    p[0](&pt2D); //it will call Draw2D function 
    p[1](&pt3D); //it will call Draw3D function 
    return 0; 
} 
+1

Esto no es una respuesta. ¿Cómo es relevante un tutorial sobre funciones? ¿Cómo se puede sustituir esto en situaciones de la vida real donde las funciones virtuales son deseables, p. El ejemplo de OP de crear/iterar un contenedor de objetos heterogéneos relacionados? ¿Cómo sabría cada obj qué función debería llamarse? Necesitaría algún tipo de ... tabla de funciones. Cualquiera que trate de implementar un polimorfismo o un OOP más amplio a la C termina con el mismo nivel de sobrecarga que si hubiera usado elementos nativos de C++ como vfuncs, excepto que ahora nadie puede entender su código. (c.f. C OOP libs: impresionante, pero una verdadera tarea para leer) –