¿Por qué el sizeof
operador devuelve un tamaño mayor para una estructura que los tamaños totales de los miembros de la estructura?
¿Por qué el sizeof
operador devuelve un tamaño mayor para una estructura que los tamaños totales de los miembros de la estructura?
Respuestas:
Esto se debe al relleno agregado para satisfacer las restricciones de alineación. La alineación de la estructura de datos afecta tanto el rendimiento como la corrección de los programas:
SIGBUS
).Aquí hay un ejemplo que usa la configuración típica para un procesador x86 (todos usan modos de 32 y 64 bits):
struct X
{
short s; /* 2 bytes */
/* 2 padding bytes */
int i; /* 4 bytes */
char c; /* 1 byte */
/* 3 padding bytes */
};
struct Y
{
int i; /* 4 bytes */
char c; /* 1 byte */
/* 1 padding byte */
short s; /* 2 bytes */
};
struct Z
{
int i; /* 4 bytes */
short s; /* 2 bytes */
char c; /* 1 byte */
/* 1 padding byte */
};
const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */
Se puede minimizar el tamaño de las estructuras clasificando los miembros por alineación (la clasificación por tamaño es suficiente para eso en los tipos básicos) (como la estructura Z
en el ejemplo anterior).
NOTA IMPORTANTE: Los estándares C y C ++ establecen que la alineación de la estructura está definida por la implementación. Por lo tanto, cada compilador puede elegir alinear los datos de manera diferente, lo que da como resultado diseños de datos diferentes e incompatibles. Por esta razón, cuando se trata de bibliotecas que serán utilizadas por diferentes compiladores, es importante comprender cómo los compiladores alinean los datos. Algunos compiladores tienen configuraciones de línea de comandos y / o #pragma
declaraciones especiales para cambiar la configuración de alineación de la estructura.
Empaque y alineación de bytes, como se describe en las Preguntas frecuentes de C aquí :
Es para la alineación. Muchos procesadores no pueden acceder a cantidades de 2 y 4 bytes (p. Ej., Entradas y entradas largas) si están repletas de todas formas.
Supongamos que tiene esta estructura:
struct { char a[3]; short int b; long int c; char d[3]; };
Ahora, podría pensar que debería ser posible empaquetar esta estructura en la memoria de esta manera:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
Pero es mucho, mucho más fácil en el procesador si el compilador lo organiza así:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
En la versión empaquetada, observe cómo es al menos un poco difícil para usted y para mí ver cómo se envuelven los campos byc. En pocas palabras, también es difícil para el procesador. Por lo tanto, la mayoría de los compiladores rellenarán la estructura (como con campos extra invisibles) de esta manera:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
entonces &s.a == &s
y &s.d == &s + 12
(dada la alineación que se muestra en la respuesta). El puntero solo se almacena si las matrices tienen un tamaño variable (por ejemplo, a
se declaró en char a[]
lugar de char a[3]
), pero luego los elementos deben almacenarse en otro lugar.
Si desea que la estructura tenga un cierto tamaño con GCC, por ejemplo, use __attribute__((packed))
.
En Windows, puede establecer la alineación en un byte cuando utiliza el compilador cl.exe con la opción / Zp .
Por lo general, es más fácil para la CPU acceder a datos que son múltiplos de 4 (u 8), dependiendo de la plataforma y también del compilador.
Entonces es una cuestión de alineación básicamente.
Necesitas tener buenas razones para cambiarlo.
Esto puede deberse a la alineación de bytes y al relleno para que la estructura tenga un número par de bytes (o palabras) en su plataforma. Por ejemplo, en C en Linux, las siguientes 3 estructuras:
#include "stdio.h"
struct oneInt {
int x;
};
struct twoInts {
int x;
int y;
};
struct someBits {
int x:2;
int y:6;
};
int main (int argc, char** argv) {
printf("oneInt=%zu\n",sizeof(struct oneInt));
printf("twoInts=%zu\n",sizeof(struct twoInts));
printf("someBits=%zu\n",sizeof(struct someBits));
return 0;
}
Tener miembros cuyos tamaños (en bytes) son 4 bytes (32 bits), 8 bytes (2x 32 bits) y 1 byte (2 + 6 bits) respectivamente. El programa anterior (en Linux usando gcc) imprime los tamaños como 4, 8 y 4, donde la última estructura está acolchada para que sea una sola palabra (4 x 8 bits en mi plataforma de 32 bits).
oneInt=4
twoInts=8
someBits=4
:2
y en :6
realidad están especificando 2 y 6 bits, no enteros enteros de 32 bits en este caso. someBits.x, siendo solo 2 bits solo puede almacenar 4 valores posibles: 00, 01, 10 y 11 (1, 2, 3 y 4). ¿Esto tiene sentido? Aquí hay un artículo sobre la función: geeksforgeeks.org/bit-fields-c
Ver también:
para Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
y GCC afirman compatibilidad con el compilador de Microsoft .:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
Además de las respuestas anteriores, tenga en cuenta que, independientemente del paquete, no hay garantía de pedido de miembros en C ++ . Los compiladores pueden (y ciertamente lo hacen) agregar puntero de tabla virtual y miembros de estructuras base a la estructura. Incluso la existencia de una tabla virtual no está garantizada por el estándar (la implementación del mecanismo virtual no está especificada) y, por lo tanto, se puede concluir que dicha garantía es simplemente imposible.
Estoy bastante seguro de que el orden de los miembros está garantizado en C , pero no contaría con él al escribir un programa multiplataforma o de compilación cruzada.
El tamaño de una estructura es mayor que la suma de sus partes debido a lo que se llama embalaje. Un procesador particular tiene un tamaño de datos preferido con el que trabaja. El tamaño preferido de la mayoría de los procesadores modernos es de 32 bits (4 bytes). Acceder a la memoria cuando los datos están en este tipo de límite es más eficiente que las cosas que abarcan ese límite de tamaño.
Por ejemplo. Considere la estructura simple:
struct myStruct
{
int a;
char b;
int c;
} data;
Si la máquina es una máquina de 32 bits y los datos están alineados en un límite de 32 bits, vemos un problema inmediato (suponiendo que no hay alineación de estructura). En este ejemplo, supongamos que los datos de la estructura comienzan en la dirección 1024 (0x400; tenga en cuenta que los 2 bits más bajos son cero, por lo que los datos están alineados con un límite de 32 bits). El acceso a data.a funcionará bien porque comienza en un límite: 0x400. El acceso a data.b también funcionará bien, porque está en la dirección 0x404, otro límite de 32 bits. Pero una estructura no alineada pondría data.c en la dirección 0x405. Los 4 bytes de data.c están en 0x405, 0x406, 0x407, 0x408. En una máquina de 32 bits, el sistema leería data.c durante un ciclo de memoria, pero solo obtendría 3 de los 4 bytes (el 4to byte está en el próximo límite). Entonces, el sistema tendría que hacer un segundo acceso a la memoria para obtener el 4to byte,
Ahora, si en lugar de poner data.c en la dirección 0x405, el compilador rellena la estructura en 3 bytes y coloca data.c en la dirección 0x408, entonces el sistema solo necesitará 1 ciclo para leer los datos, reduciendo el tiempo de acceso a ese elemento de datos en un 50%. El relleno intercambia la eficiencia de la memoria para la eficiencia del procesamiento. Dado que las computadoras pueden tener grandes cantidades de memoria (muchos gigabytes), los compiladores consideran que el intercambio (velocidad sobre el tamaño) es razonable.
Desafortunadamente, este problema se vuelve mortal cuando intenta enviar estructuras a través de una red o incluso escribir los datos binarios en un archivo binario. El relleno insertado entre los elementos de una estructura o clase puede alterar los datos enviados al archivo o la red. Para escribir código portátil (uno que irá a varios compiladores diferentes), probablemente tendrá que acceder a cada elemento de la estructura por separado para garantizar el "empaque" adecuado.
Por otro lado, diferentes compiladores tienen diferentes capacidades para administrar el empaque de la estructura de datos. Por ejemplo, en Visual C / C ++, el compilador admite el comando #pragma pack. Esto le permitirá ajustar el empaquetado y la alineación de datos.
Por ejemplo:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
Ahora debería tener una longitud de 11. Sin el pragma, podría tener entre 11 y 14 (y para algunos sistemas, hasta 32), dependiendo del empaque predeterminado del compilador.
#pragma pack
. Si los miembros se asignan en su alineación predeterminada, generalmente diría que la estructura no está empaquetada.
Puede hacerlo si ha establecido implícita o explícitamente la alineación de la estructura. Una estructura que está alineada 4 siempre será un múltiplo de 4 bytes, incluso si el tamaño de sus miembros fuera algo que no sea múltiplo de 4 bytes.
También se puede compilar una biblioteca bajo x86 con entradas de 32 bits y es posible que esté comparando sus componentes en un proceso de 64 bits que le daría un resultado diferente si lo hiciera a mano.
C99 N1256 borrador estándar
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 El tamaño del operador :
3 Cuando se aplica a un operando que tiene estructura o tipo de unión, el resultado es el número total de bytes en dicho objeto, incluidos los rellenos internos y finales.
6.7.2.1 Especificadores de estructura y unión :
13 ... Puede haber relleno sin nombre dentro de un objeto de estructura, pero no al principio.
y:
15 Puede haber relleno sin nombre al final de una estructura o unión.
La nueva función de miembro de matriz flexible C99 ( struct S {int is[];};
) también puede afectar el relleno:
16 Como caso especial, el último elemento de una estructura con más de un miembro nombrado puede tener un tipo de matriz incompleto; Esto se llama un miembro de matriz flexible. En la mayoría de las situaciones, el miembro de matriz flexible se ignora. En particular, el tamaño de la estructura es como si se omitiera el miembro de matriz flexible, excepto que puede tener más relleno posterior de lo que implicaría la omisión.
El Anexo J Cuestiones de portabilidad reitera:
Los siguientes no están especificados: ...
- El valor de los bytes de relleno al almacenar valores en estructuras o uniones (6.2.6.1)
C ++ 11 N3337 borrador estándar
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 Tamaño de :
2 Cuando se aplica a una clase, el resultado es el número de bytes en un objeto de esa clase, incluido cualquier relleno necesario para colocar objetos de ese tipo en una matriz.
9.2 Miembros de la clase :
Un puntero a un objeto de estructura de diseño estándar, convertido adecuadamente usando un reinterpret_cast, apunta a su miembro inicial (o si ese miembro es un campo de bits, luego a la unidad en la que reside) y viceversa. [Nota: Por lo tanto, puede haber relleno sin nombre dentro de un objeto de estructura de diseño estándar, pero no al principio, según sea necesario para lograr la alineación adecuada. - nota final]
Solo sé suficiente C ++ para entender la nota :-)
Además de las otras respuestas, una estructura puede (pero generalmente no) tener funciones virtuales, en cuyo caso el tamaño de la estructura también incluirá el espacio para el vtbl.
El lenguaje C deja al compilador cierta libertad sobre la ubicación de los elementos estructurales en la memoria:
El lenguaje C proporciona cierta seguridad al programador del diseño de elementos en la estructura:
Problemas relacionados con la alineación de elementos:
Cómo funciona la alineación:
ps Información más detallada está disponible aquí: "Samuel P.Harbison, Guy L.Steele CA Reference, (5.6.2 - 5.6.7)"
La idea es que, por consideraciones de velocidad y caché, los operandos deben leerse desde direcciones alineadas a su tamaño natural. Para que esto suceda, el compilador rellena los miembros de la estructura para que el siguiente miembro o la siguiente estructura se alineen.
struct pixel {
unsigned char red; // 0
unsigned char green; // 1
unsigned int alpha; // 4 (gotta skip to an aligned offset)
unsigned char blue; // 8 (then skip 9 10 11)
};
// next offset: 12
La arquitectura x86 siempre ha podido obtener direcciones desalineadas. Sin embargo, es más lento y cuando la desalineación se superpone a dos líneas de caché diferentes, desaloja dos líneas de caché cuando un acceso alineado solo desalojaría una.
Algunas arquitecturas en realidad tienen que atrapar lecturas y escrituras desalineadas, y las primeras versiones de la arquitectura ARM (la que evolucionó en todas las CPU móviles de hoy) ... bueno, en realidad solo devolvieron datos incorrectos para ellos. (Ignoraron los bits de orden inferior).
Finalmente, tenga en cuenta que las líneas de caché pueden ser arbitrariamente grandes, y el compilador no intenta adivinarlas o hacer una compensación espacio-velocidad. En cambio, las decisiones de alineación son parte de la ABI y representan la alineación mínima que eventualmente llenará de manera uniforme una línea de caché.
TL; DR: la alineación es importante.