¿Por qué sizeof para una estructura no es igual a la suma de sizeof de cada miembro?

697

¿Por qué el sizeofoperador devuelve un tamaño mayor para una estructura que los tamaños totales de los miembros de la estructura?

Kevin
fuente
14
Consulte estas preguntas frecuentes de C sobre iluminación de memoria. c-faq.com/struct/align.esr.html
Richard Chambers
48
Anécdota: Hubo un virus informático real que puso su código dentro de los rellenos de estructura en el programa host.
Elazar
44
@Elazar ¡Eso es impresionante! Nunca hubiera pensado que fuera posible usar áreas tan pequeñas para nada. ¿Puede proporcionar más detalles?
Wilson
1
@Wilson - Estoy seguro de que involucró muchos jmp.
hoodaticus
44
Ver relleno de estructura , embalaje : The Lost Art of C Estructura de embalaje Eric S. Raymond
EsmaeelE

Respuestas:

649

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:

  • El acceso mal alineado puede ser un error difícil (a menudo SIGBUS).
  • El acceso mal alineado puede ser un error leve.
    • Ya sea corregido en hardware, para una modesta degradación del rendimiento.
    • O corregido por emulación en software, para una severa degradación del rendimiento.
    • Además, la atomicidad y otras garantías de concurrencia pueden romperse y provocar errores sutiles.

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 Zen 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 #pragmadeclaraciones especiales para cambiar la configuración de alineación de la estructura.

Kevin
fuente
38
Quiero hacer una nota aquí: la mayoría de los procesadores lo penalizan por el acceso no alineado a la memoria (como mencionó), pero no puede olvidar que muchos lo rechazan por completo. La mayoría de los chips MIPS, en particular, arrojarán una excepción en un acceso no alineado.
Cody Brocious el
35
Los chips x86 son en realidad bastante únicos ya que permiten el acceso no alineado, aunque penalizado; AFAIK, la mayoría de las fichas arrojarán excepciones, no solo algunas. PowerPC es otro ejemplo común.
Dark Shikari
66
La habilitación de pragmas para accesos no alineados generalmente hace que su código aumente de tamaño, en procesadores que arrojan fallas de desalineación, ya que se debe generar un código para corregir cada desalineación. ARM también arroja fallas de desalineación.
Mike Dimmick el
55
@Dark - totalmente de acuerdo. Pero la mayoría de los procesadores de escritorio son x86 / x64, por lo que la mayoría de los chips no emiten fallas de alineación de datos;)
Aaron
27
El acceso a datos no alineados suele ser una característica que se encuentra en las arquitecturas CISC, y la mayoría de las arquitecturas RISC no lo incluyen (ARM, MIPS, PowerPC, Cell). En realidad, la mayoría de los chips NO son procesadores de escritorio, por regla incrustada por el número de chips y la gran mayoría de estos son arquitecturas RISC.
Lara Dougan
191

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  |
+-------+-------+-------+-------+
EmmEff
fuente
1
¿Cuál es el uso de las ranuras de memoria pad1, pad2 y pad3?
Lakshmi Sreekanth Chitla
77
@YoYoYonnY eso no es posible. El compilador no puede reordenar miembros de estructura aunque gcc tiene una opción experimental para hacerlo
phuclv
@EmmEff esto podría estar mal, pero no lo entiendo del todo: ¿por qué no hay una ranura de memoria para el puntero en las matrices?
Balázs Börcsök
1
@ BalázsBörcsök Se trata de matrices de tamaño constante, por lo que sus elementos se almacenan directamente en la estructura en desplazamientos fijos. El compilador sabe todo esto en tiempo de compilación, por lo que el puntero está implícito. Por ejemplo, si tiene una variable de estructura de este tipo llamada sentonces &s.a == &sy &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, ase declaró en char a[]lugar de char a[3]), pero luego los elementos deben almacenarse en otro lugar.
kbolino
27

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.

INS
fuente
55
"buenas razones" Ejemplo: mantener la compatibilidad binaria (relleno) consistente entre los sistemas de 32 y 64 bits para una estructura compleja en el código de demostración de prueba de concepto que se presentará mañana. A veces la necesidad tiene prioridad sobre la propiedad.
Mr.Ree
2
Todo está bien, excepto cuando menciona el sistema operativo. Este es un problema para la velocidad de la CPU, el sistema operativo no está involucrado en absoluto.
Blaisorblade
3
Otra buena razón es si está cargando un flujo de datos en una estructura, por ejemplo, al analizar protocolos de red.
CEO
1
@dolmen Acabo de señalar que "es más fácil para el sistema operativo acceder a los datos" es incorrecto, ya que el sistema operativo no accede a los datos.
Blaisorblade
1
@dolmen De hecho, uno debería hablar sobre la ABI (interfaz binaria de la aplicación). La alineación predeterminada (utilizada si no la cambia en el origen) depende de la ABI, y muchos sistemas operativos admiten múltiples ABI (por ejemplo, 32 y 64 bits, o para binarios de diferentes sistemas operativos, o para diferentes formas de compilar el los mismos binarios para el mismo sistema operativo). OTOH, qué alineación es conveniente en cuanto al rendimiento depende de la CPU: la memoria se accede de la misma manera si usa el modo de 32 o 64 bits (no puedo comentar sobre el modo real, pero parece poco relevante para el rendimiento hoy en día). IIRC Pentium comenzó a preferir la alineación de 8 bytes.
Blaisorblade
15

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
Kyle Burton
fuente
44
"C en Linux usando gcc" no es suficiente para describir su plataforma. La alineación depende principalmente de la arquitectura de la CPU.
dolmen
- @ Kyle Burton. Disculpe, no entiendo por qué el tamaño de la estructura "someBits" es igual a 4, espero 8 bytes ya que hay 2 enteros declarados (2 * sizeof (int)) = 8 bytes. gracias
youpilat13
1
Hola @ youpilat13, :2y en :6realidad 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
Kyle Burton el
11

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.

lkanab
fuente
44
"Estoy bastante seguro de que el orden de los miembros está gruñido en C". Sí, C99 dice: "Dentro de un objeto de estructura, los miembros que no son campos de bits y las unidades en las que residen los campos de bits tienen direcciones que aumentan en el orden en que se declaran". Más bondad estándar en: stackoverflow.com/a/37032302/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

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.

sid1138
fuente
Esto discute las consecuencias del relleno de estructura, pero no responde la pregunta.
Keith Thompson
" ... por lo que se llama embalaje ... ... creo que te refieres a" relleno "." Tamaño preferido de la mayoría de los procesadores modernos si 32 bits (4 bytes) "- Eso es una simplificación excesiva. se admiten tamaños de 8, 16, 32 y 64 bits; a menudo cada tamaño tiene su propia alineación. Y no estoy seguro de que su respuesta agregue información nueva que aún no esté en la respuesta aceptada.
Keith Thompson
1
Cuando dije empaquetar, quise decir cómo el compilador empaqueta los datos en una estructura (y puede hacerlo rellenando los elementos pequeños, pero no necesita rellenar, pero siempre empaca). En cuanto al tamaño, estaba hablando de la arquitectura del sistema, no de lo que el sistema admitirá para el acceso a datos (que es muy diferente de la arquitectura del bus subyacente). En cuanto a su comentario final, di una explicación simplificada y ampliada de un aspecto de la compensación (velocidad versus tamaño): un problema importante de programación. También describo una forma de solucionar el problema, que no estaba en la respuesta aceptada.
sid1138
"Empaque" en este contexto generalmente se refiere a la asignación de miembros más estrictamente que el predeterminado, como con #pragma pack. Si los miembros se asignan en su alineación predeterminada, generalmente diría que la estructura no está empaquetada.
Keith Thompson
Embalaje es una especie de término sobrecargado. Significa cómo poner elementos de estructura en la memoria. Similar al significado de poner objetos en una caja (empacar para mover). También significa poner elementos en la memoria sin relleno (una especie de mano corta para "apretado"). Luego está la versión de comando de la palabra en el comando #pragma pack.
sid1138
5

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.

Orion Adrian
fuente
5

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 :-)

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
4

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.

JohnMcG
fuente
8
No exactamente. En implementaciones típicas, lo que se agrega a la estructura es un puntero vtable .
Don Wakefield
3

El lenguaje C deja al compilador cierta libertad sobre la ubicación de los elementos estructurales en la memoria:

  • Los agujeros de memoria pueden aparecer entre dos componentes, y después del último componente. Se debió al hecho de que ciertos tipos de objetos en la computadora de destino pueden estar limitados por los límites de direccionamiento
  • Tamaño de "agujeros de memoria" incluido en el resultado del tamaño del operador. El tamaño de solo no incluye el tamaño de la matriz flexible, que está disponible en C / C ++
  • Algunas implementaciones del lenguaje le permiten controlar el diseño de memoria de las estructuras a través de las opciones de pragma y compilador.

El lenguaje C proporciona cierta seguridad al programador del diseño de elementos en la estructura:

  • se requieren compiladores para asignar una secuencia de componentes aumentando las direcciones de memoria
  • La dirección del primer componente coincide con la dirección inicial de la estructura.
  • los campos de bits sin nombre pueden incluirse en la estructura para las alineaciones de direcciones requeridas de elementos adyacentes

Problemas relacionados con la alineación de elementos:

  • Diferentes computadoras alinean los bordes de los objetos de diferentes maneras
  • Diferentes restricciones en el ancho del campo de bits
  • Las computadoras difieren en cómo almacenar los bytes en una palabra (Intel 80x86 y Motorola 68000)

Cómo funciona la alineación:

  • El volumen ocupado por la estructura se calcula como el tamaño del elemento individual alineado de una matriz de tales estructuras. La estructura debe terminar de modo que el primer elemento de la siguiente estructura no viole los requisitos de 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)"

bruziuz
fuente
2

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.

DigitalRoss
fuente