programas programación programa paso para ingenieros ejemplos dev completo comandos basicos c++ c struct memory-alignment

c++ - programación - Struct Reordenamiento por compilador



programa c++ (8)

Supongamos que tengo una estructura como esta:

struct MyStruct { uint8_t var0; uint32_t var1; uint8_t var2; uint8_t var3; uint8_t var4; };

Esto posiblemente va a desperdiciar un montón (bueno, no una tonelada) de espacio. Esto se debe a la alineación necesaria de la variable uint32_t .

En realidad (después de alinear la estructura para que realmente pueda usar la variable uint32_t ), podría verse más o menos así:

struct MyStruct { uint8_t var0; uint8_t unused[3]; //3 bytes of wasted space uint32_t var1; uint8_t var2; uint8_t var3; uint8_t var4; };

Una estructura más eficiente sería:

struct MyStruct { uint8_t var0; uint8_t var2; uint8_t var3; uint8_t var4; uint32_t var1; };

Ahora, la pregunta es:

¿Por qué el compilador está prohibido (según el estándar) de reordenar la estructura?

No veo la forma en que puedas dispararte en el pie si la estructura fue reordenada.


¿Por qué el compilador está prohibido (según el estándar) de reordenar la estructura?

La razón básica es: por compatibilidad con C.

Recuerde que C es, originalmente, un lenguaje ensamblador de alto nivel. Es bastante común en C ver la memoria (paquetes de red, ...) al reinterpretar los bytes como una struct específica.

Esto ha llevado a múltiples características que dependen de esta propiedad:

  • C garantizó que la dirección de una struct y la dirección de su primer miembro de datos son una y la misma, por lo que C ++ también (en ausencia de herencia virtual / métodos).

  • C garantiza que si tiene dos struct A y B y ambas comienzan con un miembro de datos char seguido de un miembro de datos int (y lo que sea después), cuando los coloca en una union puede escribir el miembro B y leer el char y int través de su miembro A , por lo que C ++ también: Diseño estándar .

Este último es extremadamente amplio e impide completamente cualquier reordenamiento de los miembros de datos para la mayoría de las struct (o class ).

Tenga en cuenta que el estándar permite algunos reordenamientos: dado que C no tenía el concepto de control de acceso, C ++ especifica que el orden relativo de dos miembros de datos con un especificador de control de acceso diferente no está especificado.

Hasta donde yo sé, ningún compilador intenta aprovecharlo; pero podrían en teoría.

Fuera de C ++, los lenguajes como Rust permiten a los compiladores reordenar campos y el compilador principal de Rust (rustc) lo hace de forma predeterminada. Solo decisiones históricas y un fuerte deseo de compatibilidad con versiones anteriores evitan que C ++ lo haga.


No veo la forma en que puedas dispararte en el pie, si la estructura fue reordenada.

De Verdad? Si esto estuviera permitido, la comunicación entre bibliotecas / módulos incluso en el mismo proceso sería ridículamente peligrosa por defecto.

Argumento "en el universo"

Debemos ser capaces de saber que nuestras estructuras están definidas de la manera que les hemos pedido que sean. ¡Ya es suficientemente malo que el relleno no esté especificado! Afortunadamente, puedes controlar esto cuando lo necesites.

De acuerdo, teóricamente, se podría hacer un nuevo lenguaje de modo que, de manera similar, los miembros pudieran ser ordenados a menos que se les diera algún atributo . Después de todo, se supone que no debemos hacer magia a nivel de memoria en los objetos, por lo que si solo usáramos modismos C ++, estaríamos seguros por defecto.

Pero esa no es la realidad práctica en la que vivimos.

Argumento "fuera del universo"

Podría hacer las cosas seguras si, según sus palabras, "se usara el mismo pedido cada vez". El lenguaje debería indicar inequívocamente cómo se ordenarían los miembros. Es complicado escribir en el estándar, complicado de entender y complicado de implementar.

Es mucho más fácil simplemente garantizar que el orden será como está en el código, y dejar estas decisiones al programador. Recuerde, estas reglas tienen su origen en la antigua C, y la antigua C le da poder al programador .

Ya has mostrado en tu pregunta lo fácil que es hacer que el relleno de estructura sea eficiente con un cambio de código trivial. No hay necesidad de ninguna complejidad adicional en el nivel de idioma para hacer esto por usted.


El compilador debe mantener el orden de sus miembros en el caso de que las estructuras sean leídas por cualquier otro código de bajo nivel producido por otro compilador u otro idioma. Supongamos que está creando un sistema operativo y decide escribir parte de él en C y parte en ensamblaje. Podrías definir la siguiente estructura:

struct keyboard_input { uint8_t modifiers; uint32_t scancode; }

Pasa esto a una rutina de ensamblaje, donde debe especificar manualmente el diseño de memoria de la estructura. Es de esperar que pueda escribir el siguiente código en un sistema con alineación de 4 bytes.

; The memory location of the structure is located in ebx in this example mov al, [ebx] mov edx, [ebx+4]

Ahora diga que el compilador cambiaría el orden de los miembros en la estructura de una manera definida de implementación, esto significaría que dependiendo del compilador que use y las banderas que le pase, podría terminar con el primer byte del scancode miembro en al, o con el miembro modificadores.

Por supuesto, el problema no solo se reduce a interfaces de bajo nivel con rutinas de ensamblaje, sino que también aparecería si las bibliotecas creadas con compiladores diferentes se llamaran entre sí (por ejemplo, construir un programa con mingw usando la API de Windows).

Debido a esto, el lenguaje simplemente te obliga a pensar sobre el diseño de la estructura.


El lenguaje diseñado por Dennis Ritchie definió la semántica de las estructuras no en términos de comportamiento, sino en términos de diseño de la memoria. Si una estructura S tenía un miembro M de tipo T en desplazamiento X, entonces el comportamiento de MS se definió como tomando la dirección de S, añadiéndole X bytes, interpretándolo como un puntero a T e interpretando el almacenamiento identificado como un lvalue Escribir un miembro de estructura cambiaría el contenido de su almacenamiento asociado, y cambiar el contenido del almacenamiento de un miembro cambiaría el valor de un miembro. El código era libre de usar una amplia variedad de formas de manipular el almacenamiento asociado con los miembros de la estructura, y la semántica se definiría en términos de operaciones en ese almacenamiento.

Entre las formas útiles en que el código podría manipular el almacenamiento asociado con una estructura, se encontraba el uso de memcpy () para copiar una parte arbitraria de una estructura en una porción correspondiente de otra, o memset () para borrar una parte arbitraria de una estructura. Como los miembros de la estructura se distribuyeron secuencialmente, se podía copiar o borrar un rango de miembros usando una única llamada memcpy () o memset ().

El lenguaje definido por el Comité Estándar elimina en muchos casos el requisito de que los cambios en los miembros de la estructura deben afectar el almacenamiento subyacente, o que los cambios en el almacenamiento afectan los valores de los miembros, haciendo que las garantías sobre el diseño de la estructura sean menos útiles que en el lenguaje de Ritchie. No obstante, la capacidad de utilizar memcpy () y memset () se mantuvo, y retener esa capacidad requiere mantener los elementos de la estructura en secuencia.


Imagine que este diseño de estructura es en realidad una secuencia de memoria recibida ''por cable'', digamos un paquete de Ethernet. si el compilador re-alineó las cosas para ser más eficiente, entonces tendría que hacer un montón de trabajo extrayendo bytes en el orden requerido, en lugar de simplemente usar una estructura que tenga todos los bytes correctos en el orden y lugar correctos.


La norma garantiza una orden de asignación simplemente porque las estructuras pueden representar un cierto diseño de memoria, como un protocolo de datos o una colección de registros de hardware. Por ejemplo, ni el programador ni el compilador pueden reorganizar el orden de los bytes en el protocolo TPC / IP o los registros de hardware de un microcontrolador.

Si la orden no estuviera garantizada, las structs serían meros contenedores de datos abstractos (similares al vector C ++), de los cuales no podemos suponer mucho, excepto que de alguna manera contienen los datos que ponemos dentro de ellos. Los haría sustancialmente más inútiles al hacer cualquier forma de programación de bajo nivel.


Recuerde que no solo el reordenamiento automático de los elementos para mejorar el empaque puede funcionar en detrimento de diseños de memoria específicos o serialización binaria, sino que el orden de las propiedades puede haber sido cuidadosamente elegido por el programador para beneficiar a la caché de los miembros de uso frecuente contra cuanto más raramente se accede.


También cita C ++, así que le daré un motivo práctico por el que eso no puede suceder.

Dado que no hay diferencia entre class y struct , considere:

class MyClass { string s; anotherObject b; MyClass() : s{"hello"}, b{s} {} };

Ahora C ++ requiere que los miembros de datos no estáticos se inicialicen en el orden en que fueron declarados:

- Luego, los miembros de datos no estáticos se inicializan en el orden en que se declararon en la definición de clase

según [base.class.init/13] . Por lo tanto, el compilador no puede reordenar campos dentro de la definición de la clase, porque de lo contrario (como un ejemplo) los miembros que dependen de la inicialización de otros no podrían funcionar.

El compilador no es estrictamente necesario, no reordenarlos en la memoria (por lo que puedo decir), pero, especialmente teniendo en cuenta el ejemplo anterior, sería terriblemente doloroso hacer un seguimiento de eso. Y dudo de mejoras en el rendimiento, a diferencia del relleno.