tag etiquetas etiqueta c++ c struct memory-alignment

c++ - etiquetas - Al extender una estructura acolchada, ¿por qué no se pueden colocar campos adicionales en el relleno de la cola?



etiqueta mailchimp (4)

Consideremos las estructuras:

struct S1 { int a; char b; }; struct S2 { struct S1 s; /* struct needed to make this compile as C without typedef */ char c; }; // For the C++ fans struct S3 : S1 { char c; };

El tamaño de S1 es 8, lo cual se espera debido a la alineación. Pero el tamaño de S2 y S3 es 12. Lo que significa que el compilador los estructura como:

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| | a | b | padding | c | padding |

El compilador podría colocar c en el relleno en 6 7 8 sin romper las restricciones de alineación. ¿Cuál es la regla que lo previene y cuál es la razón detrás de esto?


¿Cuál es la razón detrás del relleno adicional en las estructuras?

Si el procesador habla en serio acerca de la alineación , genera una excepción / señal, de lo contrario una penalización de rendimiento estará allí como acceso de datos de desaceleración de desajuste.

Para entender esto comencemos con la alineación de la estructura de datos :

La alineación de la estructura de datos es la forma en que se organizan los datos y se accede a ellos en la memoria de la computadora. Consiste en dos problemas separados pero relacionados: alineación de datos y relleno de la estructura de datos . Cuando una computadora moderna lee o escribe en una dirección de memoria, lo hará en trozos del tamaño de una palabra (por ejemplo, trozos de 4 bytes en un sistema de 32 bits) o más grande. La alineación de datos significa poner los datos en un desplazamiento de memoria igual a un múltiplo del tamaño de palabra, lo que aumenta el rendimiento del sistema debido a la forma en que la CPU maneja la memoria . Para alinear los datos, puede ser necesario insertar algunos bytes sin sentido entre el final de la última estructura de datos y el inicio de la siguiente, que es el relleno de la estructura de datos.

Por ejemplo, cuando el tamaño de palabra de la computadora es de 4 bytes (un byte significa 8 bits en la mayoría de las máquinas, pero podría ser diferente en algunos sistemas), los datos que se leerán estarán en un desplazamiento de memoria que es un múltiplo de 4. Cuando esto no es el caso, por ejemplo, los datos comienzan en el decimocuarto byte en lugar del 16º, entonces la computadora tiene que leer dos fragmentos de 4 bytes y hacer algunos cálculos antes de que se lean los datos solicitados, o puede generar un error de alineación . Aunque la estructura de datos anterior termina en el decimotercer byte, la siguiente estructura de datos debería comenzar en el decimosexto byte. Se insertan dos bytes de relleno entre las dos estructuras de datos para alinear la siguiente estructura de datos con el 16 ° byte.

Al extender una estructura acolchada, ¿por qué no se pueden colocar campos adicionales en el relleno de la cola?

El compilador podría colocar c en el relleno en 6 7 8 sin romper las restricciones de alineación. ¿Cuál es la regla que lo previene y cuál es la razón detrás de esto?

El compilador podría colocarlo allí pero luego el acceso a la memoria a c se alineará incorrectamente y habrá una penalización de rendimiento como se explicó anteriormente. Para aliar una matriz:

struct __attribute__((__packed__)) mypackedstruct{ char a; int b; char c; };

Esta estructura tendría un tamaño compilado de 6 bytes en un sistema de 32 bits.
El acceso a la memoria no alineada es más lento en las arquitecturas que lo permiten (como x86 y amd64), y está explícitamente prohibido en arquitecturas de alineación estrictas como SPARC.

1 Se dice que un acceso a la memoria está alineado cuando el dato al que se accede tiene n bytes de longitud (donde n es una potencia de 2) y la dirección de referencia está n -alineada por bytes. Cuando el acceso a la memoria no está alineado, se dice que está desalineado.


Aquí hay un par de ejemplos de por qué un compilador no puede colocar el miembro c en el relleno posterior de los miembros de la struct S1 . Supongamos por lo siguiente que el compilador struct S2.c en el relleno de la struct S1.s. miembro:

struct S1 { int a; char b; }; struct S2 { struct S1 s; /* struct needed to make this compile as C without typedef */ char c; }; // ... struct S1 foo = { 10, ''a'' }; struct S2 bar = {{ 20, ''b''}, ''c'' }; bar.s = foo; // this will likely corrupt bar.c memcpy(&bar.s, &foo, sizeof(bar.s)); // this will certainly corrupt bar.c bar.s.b = ''z''; // this is permited to corrupt bar by C99 6.2.6.1/6

C99 / C11 6.2.6.1/6 ("Representación de tipos / general") dice:

Cuando se almacena un valor en un objeto de estructura o tipo de unión, incluido en un objeto miembro, los bytes de la representación de objeto que corresponden a cualquier byte de relleno toman valores no especificados.


Consideremos algún código:

struct S1 { int a; char b; }; struct S2 { S1 s; char c; };

Consideremos qué pasaría si sizeof(S1) == 8 y sizeof(S2) == 8 .

struct S2 s2; struct S1 *s1 = &(s2.s); memset(s1, 0, sizeof(*s1));

Ahora ha sobrescrito S2::c .

Para motivos de alineación de matriz, S2 tampoco puede tener un tamaño de 9, 10 o 11. Por lo tanto, el siguiente tamaño válido es 12.


Respuesta corta (para la parte C ++ de la pregunta) : El Itanium ABI para C ++ prohíbe, por razones históricas, usar el relleno de cola de un subobjeto base de tipo POD. Tenga en cuenta que C ++ 11 no tiene tal prohibición. La regla relevante 3.9 / 2 que permite copiar tipos trivialmente copiables a través de su representación subyacente excluye explícitamente los subobjetos base.

Respuesta larga: intentaré y trataré C ++ 11 y C de una vez.

  1. El diseño de S1 debe incluir relleno, ya que S1::a debe estar alineado para int , y una matriz S1[N] consta de objetos contiguos asignados de tipo S1 , cada uno de cuyos miembros debe estar alineado.
  2. En C ++, los objetos de tipo T trivialmente copulables que no son subobjetos base se pueden tratar como matrices de sizeof(T) bytes (es decir, puede convertir un puntero de objeto en un unsigned char * y tratar el resultado como un puntero al primero elemento de un unsigned char[sizeof(T)] , y el valor de esta matriz determina el objeto). Como todos los objetos en C son de este tipo, esto explica S2 para C y C ++.
  3. Los casos interesantes restantes para C ++ son:
    1. Subobjetos base, que no están sujetos a la regla anterior (véase C ++ 11 3.9 / 2), y
    2. cualquier objeto que no sea del tipo de copia trivial.

Para 3.1, de hecho, existen "optimizaciones de diseño base" comunes y populares en las que los compiladores "comprimen" los miembros de datos de una clase en los subobjetos base. Esto es más llamativo cuando la clase base está vacía (∞% de reducción de tamaño!), Pero se aplica de manera más general. Sin embargo, el Itanium ABI para C ++ que vinculé anteriormente y que implementan muchos compiladores, prohíbe la compresión del relleno de cola cuando el tipo de base respectivo es POD (y POD significa copiable trivialmente y diseño estándar).

Para 3.2 se aplica la misma parte de Itanium ABI, aunque actualmente no creo que el estándar C ++ 11 en realidad establezca que los objetos miembros arbitrarios que no pueden copiarse trivialmente deben tener el mismo tamaño que un objeto completo del mismo tipo .

Respuesta anterior guardada para referencia.

Creo que esto se debe a que S1 es de diseño estándar, por lo que, por alguna razón, el objeto S1 de S3 permanece intacto. No estoy seguro de si eso es obligatorio por el estándar.

Sin embargo, si convertimos S1 en un diseño no estándar, observamos una optimización del diseño:

struct EB { }; struct S1 : EB { // not standard-layout EB eb; int a; char b; }; struct S3 : S1 { char c; };

Ahora sizeof(S1) == sizeof(S3) == 12 en mi plataforma. Demostración en vivo

Y aquí hay un ejemplo más simple :

struct S1 { private: int a; public: char b; }; struct S3 : S1 { char c; };

El acceso mixto hace que S1 no tenga un diseño estándar. (Ahora sizeof(S1) == sizeof(S3) == 8 )

Actualización: El factor de definición parece ser la trivialidad , así como la disposición estándar, es decir, la clase debe ser POD. La siguiente clase de diseño estándar que no es POD tiene un diseño base optimizable:

struct S1 { ~S1(){} int a; char b; }; struct S3 : S1 { char c; };

Nuevamente sizeof(S1) == sizeof(S3) == 8 . Demo