2009-03-15 8 views
17

Estoy tratando de entender qué tipo de memoria golpearé al crear una gran variedad de objetos. Sé que cada objeto, cuando se cree, tendrá espacio en el HEAP para las variables miembro, y creo que todo el código para cada función que pertenece a ese tipo de objeto existe en el segmento de código en la memoria, de forma permanente.En C++, ¿en qué memoria están las funciones de clase?

¿Es correcto?

Así que si creo 100 objetos en C++, puedo estimar que necesitaré espacio para todas las variables miembro que posee ese objeto multiplicado por 100 (posibles problemas de alineación aquí), y luego necesito espacio en el segmento de código para una sola copia del código para cada función miembro para ese tipo de objeto (no 100 copias del código).

¿Las funciones virtuales, el polimorfismo, el factor de herencia en esto de alguna manera?

¿Qué pasa con los objetos de bibliotecas vinculadas dinámicamente? Supongo que los dlls obtienen sus propios segmentos de pila, montón, código y datos.

ejemplo simple (no puede ser sintácticamente correcta):

// parent class 
class Bar 
{ 
public: 
    Bar() {}; 
    ~Bar() {}; 

    // pure virtual function 
    virtual void doSomething() = 0; 

protected: 
    // a protected variable 
    int mProtectedVar; 
} 

// our object class that we'll create multiple instances of 
class Foo : public Bar 
{ 
public: 
    Foo() {}; 
    ~Foo() {}; 

    // implement pure virtual function 
    void doSomething()   { mPrivate = 0; } 

    // a couple public functions 
    int getPrivateVar()   { return mPrivate; } 
    void setPrivateVar(int v) { mPrivate = v; } 

    // a couple public variables 
    int mPublicVar; 
    char mPublicVar2; 

private: 
    // a couple private variables 
    int mPrivate; 
    char mPrivateVar2;   
} 

Acerca de la cantidad de memoria deben 100 objetos asignados dinámicamente de tipo Foo tomar incluyendo espacio para el código y todas las variables?

Respuesta

26

No es necesariamente cierto que "cada objeto, cuando se cree, tendrá espacio en el HEAP para las variables miembro". Cada objeto que cree tomará un espacio distinto de cero en alguna parte para sus variables miembro, pero depende de cómo asigne el objeto en sí. Si el objeto tiene asignación automática (pila), también lo harán sus miembros de datos. Si el objeto se asigna en la tienda gratuita (heap), también lo serán sus miembros de datos. Después de todo, ¿cuál es la asignación de un objeto que no sea el de sus miembros de datos?

Si un objeto asignado a la pila contiene un puntero u otro tipo que luego se usa para asignar en el montón, esa asignación se producirá en el montón, independientemente de dónde se haya creado el objeto.

Para objetos con funciones virtuales, cada uno tendrá un puntero vtable asignado como si fuera un miembro de datos explícitamente declarado dentro de la clase.

En cuanto a las funciones miembro, el código para ellas probablemente no difiere del código de función libre en términos de a dónde va en la imagen ejecutable. Después de todo, una función miembro es básicamente una función libre con un puntero "this" implícito como primer argumento.

La herencia no cambia mucho de nada.

No estoy seguro de lo que quiere decir con las DLL que obtienen su propia pila. Una DLL no es un programa, y ​​no debería necesitar una pila (o pila), ya que los objetos que asigna siempre se asignan en el contexto de un programa que tiene su propia pila y pila. Que haya código (texto) y segmentos de datos en un archivo DLL tiene sentido, aunque no soy experto en la implementación de tales cosas en Windows (que supongo que está usando su terminología).

+0

Su hipótesis "Si el objeto tiene asignación automática (pila), también lo harán sus miembros de datos" no es correcto. Tome un std :: vector que se declara como una variable de pila. Si inserta nuevos valores en el vector, los asignará en el montón, no en la pila – newgre

+5

Es por eso que también dije en mi respuesta "Si un objeto asignado a la pila contiene un puntero u otro tipo que luego se usa para asignar en el montón, esa asignación se producirá en el montón independientemente de dónde se creó el objeto en sí ". std :: vector comúnmente se implementa como que contiene tres punteros. –

+0

tienes razón, malentendí tu publicación – newgre

2

Aunque algunos aspectos de esto dependen del proveedor del compilador. Todo el código compilado entra en una sección de la memoria en la mayoría de los sistemas llamados 'texto'. esto está separado tanto de las secciones de montón como de apilamiento (una cuarta sección, 'datos', contiene la mayoría de las constantes). La instancia de muchas instancias de una clase incurre en espacio de tiempo de ejecución solo para sus variables de instancia, no para ninguna de sus funciones.Si hace uso de métodos virtuales, obtendrá un bit de memoria adicional, pero pequeño, reservado para la tabla de búsqueda virtual (o equivalente para compiladores que utilizan algún otro concepto), pero su tamaño está determinado por el número de métodos virtuales multiplicados por el número de clases virtuales, y es independiente del número de instancias en tiempo de ejecución

Esto es cierto para el código estático y dinámicamente vinculado. El código real vive en una región de 'texto'. La mayoría de los sistemas operativos en realidad pueden compartir el código dll en múltiples aplicaciones, por lo que si varias aplicaciones usan el mismo dll, solo una copia reside en la memoria y ambas aplicaciones pueden usarla. Obviamente, no hay ahorros adicionales de la memoria compartida si solo una aplicación usa el código vinculado.

+0

Tenga en cuenta que "herencia virtual" tiene un significado específico que es diferente de lo que probablemente quiso decir ("métodos virtuales"). –

+0

Hmm ... tienes razón ... ¡editado! – SingleNegationElimination

1

No se puede decir con exactitud cuánta memoria tomará una clase o objetos X en la RAM.

Sin embargo, para responder a sus preguntas, tiene la certeza de que el código existe solo en un lugar, nunca se "asigna". El código es, por lo tanto, por clase, y existe tanto si crea objetos como si no. El tamaño del código lo determina su compilador, e incluso a menudo se les puede decir a los compiladores que optimicen el tamaño del código, lo que lleva a resultados diferentes.

Las funciones virtuales no son diferentes, excepto la (pequeña) sobrecarga general de una tabla de método virtual, que generalmente es por clase.

En cuanto a las DLL y otras bibliotecas ... las reglas no son diferentes según el origen del código, por lo que este no es un factor en el uso de la memoria.

5

El código existe en el segmento de texto, y la cantidad de código generado en función de las clases es razonablemente complejo. Una clase aburrida sin herencia virtual ostensiblemente tiene algún código para cada función miembro (incluidos los que se crean implícitamente cuando se omiten, como los constructores de copia) una sola vez en el segmento de texto. El tamaño de cualquier instancia de clase es, como ha dicho, generalmente el tamaño de la suma de las variables miembro.

Entonces, se pone algo complejo. Algunos de los problemas son ...

  • El compilador puede, si quiere o se le indica, el código en línea. Por lo tanto, aunque podría ser una función simple, si se utiliza en muchos lugares y se elige para enlining, se puede generar una gran cantidad de código (distribuido por todo el código del programa).
  • La herencia virtual aumenta el tamaño de polimórficos de cada miembro. El VTABLE (tabla virtual) se oculta junto con cada instancia de una clase utilizando un método virtual, que contiene información para el despacho en tiempo de ejecución. Esta tabla puede crecer bastante si tiene muchas funciones virtuales o herencia múltiple (virtual). Aclaración: el VTABLE es por clase, pero los punteros al VTABLE se almacenan en cada instancia (dependiendo de la estructura de tipo ancestral del objeto).
  • Las plantillas pueden causar la hinchazón del código. Cada uso de una clase con plantilla con un nuevo conjunto de parámetros de plantilla puede generar código completamente nuevo para cada miembro. Los compiladores modernos intentan colapsar esto tanto como sea posible, pero es difícil.
  • La alineación/relleno de estructuras puede hacer que las instancias de clase simples sean más grandes de lo esperado, ya que el compilador rellena la estructura para la arquitectura de destino.

Al programar, use el operador sizeof para determinar el tamaño del objeto, nunca el código difícil. Use la métrica aproximada de "Suma del tamaño variable del miembro + algunos VTABLE (si existe)" al estimar qué tan caros serán los grupos grandes de instancias, y no se preocupe demasiado por el tamaño del código. Optimice más tarde, y si alguno de los problemas no obvios vuelve a significar algo, estaré bastante sorprendido.

+0

Adam: Debe mencionar que la tara de tamaño de VTable se realiza en base a * tipo *, no por * objeto *, aunque la clase debe señalarlo (por lo tanto, sí, por lo tanto, necesita almacenar una puntero para cada clase, pero eso es todo). – Arafangion

+0

Cierto, aclararé esto. Pero la herencia múltiple y la herencia virtual pueden incurrir en más de un puntero VTABLE por instancia. –

+0

¡Gracias por agregar detalles a las respuestas a esta pregunta! Respuesta muy útil y muy bien organizada. – petrocket

0

Su presupuesto es exacto en el caso base que ha presentado. Cada objeto también tiene un vtable con punteros para cada función virtual, por lo que se espera una memoria extra de puntero para cada función virtual.

Las variables miembro (y las funciones virtuales) de cualquier clase base también son parte de la clase, por lo tanto, inclúyalas.

Al igual que en c puede usar el operador sizeof (classname/datatype) para obtener el tamaño en bytes de una clase.

0

Sí, así es, el código no se duplica cuando se crea una instancia de objeto. En lo que respecta a las funciones virtuales, la llamada de función adecuada se determina utilizando vtable, pero eso no afecta a la creación de objetos per se.

Las DLL (bibliotecas compartidas/dinámicas en general) se mapean en memoria en el espacio de memoria del proceso. Cada modificación se realiza como Copia en escritura (COW): una sola DLL se carga una sola vez en la memoria y para cada escritura en un espacio mutable se crea una copia de ese espacio (generalmente en tamaño de página).

1

si está compilado como 32 bit. entonces sizeof (Bar) debería producir 4. Foo debería agregar 10 bytes (2 ints + 2 caracteres).

Dado que Foo es heredado de Bar. Eso es al menos 4 + 10 bytes = 14 bytes.

GCC tiene atributos para empaquetar las estructuras para que no haya relleno. En este caso, 100 entradas tomarían 1400 bytes + una pequeña sobrecarga para alinear la asignación + algunos gastos generales de administración de memoria.

Si no se especifica ningún atributo empaquetado, depende de la alineación de los compiladores.

Pero esto no tiene en cuenta la cantidad de memoria que ocupa vtable y el tamaño del código compilado.

+0

¡Gracias por hacer los cálculos! Muy útil perspectiva gcc también. – petrocket

0

Es muy difícil dar una respuesta exacta a yoour cuestión, ya que esto es implementtaion dependiente, pero los valores aproximados para una aplicación de 32 bits podría ser:

int Bar::mProtectedVar; // 4 bytes 
int Foo::mPublicVar;  // 4 bytes 
char Foo::mPublicVar2;  // 1 byte 

hay cuestiones allgnment aquí y el total final bien puede ser de 12 bytes. También tendrá un vptr - digamos anoter 4 bytes. Entonces, el tamaño total de los datos es de alrededor de 16 bytes por instancia. Es imposible decir cuánto espacio ocupará el código, pero tiene razón al pensar que solo hay una copia del código compartida entre todas las instancias.

Cuando se le pregunta

Asumo dlls obtienen segmentos de su propia pila, montón, código y datos.

La respuesta es que realmente no hay mucha diferencia entre los datos en una DLL y los datos en una aplicación; básicamente comparten todo entre ellos, esto tiene que ser así cuando lo piensas, si tenían diferentes pilas (por ejemplo) ¿cómo podrían funcionar las llamadas de función?

+0

Gracias por mostrar las cantidades de memoria (en una PC típica) y por aclarar el problema relacionado con la DLL. – petrocket

1

La información dada anteriormente es de gran ayuda y me dio una idea de la estructura de la memoria C++. Pero me gustaría agregar aquí que no importa cuántas funciones virtuales haya en una clase, siempre habrá solo 1 VPTR y 1 VTABLE por clase. Después de todos los puntos de VPTR para el VTABLE, entonces no hay necesidad de más de un VPTR en caso de múltiples funciones virtuales.

Cuestiones relacionadas