Relleno final en C/C++ en estructuras anidadas: ¿es necesario?

6 minutos de lectura

Avatar de usuario de Dom324
Dom324

Esta es más una pregunta teórica. Estoy familiarizado con cómo funciona el relleno y el relleno final.

struct myStruct{
    uint32_t x;
    char*    p;
    char     c;
};

// myStruct layout will compile to
// x:       4 Bytes
// padding: 4 Bytes
// *p:      8 Bytes
// c:       1 Byte
// padding: 7 Bytes
// Total:   24 Bytes

Tiene que haber relleno después xde modo que *p está alineado, y es necesario que haya un relleno final después c para que todo el tamaño de la estructura sea divisible por 8 (para obtener la longitud de zancada correcta). Pero considera este ejemplo:

struct A{
    uint64_t x;
    uint8_t  y;
};

struct B{
    struct A myStruct;
    uint32_t c;
};

// Based on all information I read on internet, and based on my tinkering
// with both GCC and Clang, the layout of struct B will look like:
// myStruct.x:       8 Bytes
// myStruct.y:       1 Byte
// myStruct.padding: 7 Bytes
// c:                4 Bytes
// padding:          4 Bytes
// total size:       24 Bytes
// total padding:    11 Bytes
// padding overhead: 45%

// my question is, why struct A does not get "inlined" into struct B,
// and therefore why the final layout of struct B does not look like this:
// myStruct.x:       8 Bytes
// myStruct.y:       1 Byte
// padding           3 Bytes
// c:                4 Bytes
// total size:       16 Bytes
// total padding:    3 Bytes
// padding overhead: 19%

Ambos diseños satisfacen las alineaciones de todas las variables. Ambos diseños tienen el mismo orden de variables. En ambos diseños struct B tiene la longitud de zancada correcta (divisible por 8 bytes). Lo único que difiere (además de un tamaño un 33 % más pequeño) es que struct A no tiene la longitud de zancada correcta en el diseño 2, pero eso no debería importar, ya que claramente no hay una matriz de struct As.

Revisé este diseño en GCC con -O3 y -g, struct B tiene 24 Bytes.

Mi pregunta es: ¿hay alguna razón por la que no se aplique esta optimización? ¿Hay algún requisito de diseño en C/C++ que prohíba esto? ¿O hay algún indicador de compilación que me falta? ¿O es esto una cosa ABI?

EDITAR: Respondido.

  1. Vea la respuesta de @dbush sobre por qué el compilador no puede emitir este diseño por sí solo.
  2. El siguiente ejemplo de código utiliza pragmas GCC packed y aligned (como lo sugiere @jaskij) para aplicar manualmente el diseño más optimizado. estructura B_packed tiene solo 16 bytes en lugar de 24 bytes (tenga en cuenta que este código puede causar problemas o funcionar lentamente cuando hay una serie de estructuras B_packedtenga cuidado y no copie ciegamente este código):
struct __attribute__ ((__packed__)) A_packed{
    uint64_t x;
    uint8_t  y;
};

struct __attribute__ ((__packed__)) B_packed{
    struct A_packed myStruct;
    uint32_t c __attribute__ ((aligned(4)));
};

// Layout of B_packed will be
// myStruct.x:       8 Bytes
// myStruct.y:       1 Byte
// padding for c:    3 Bytes
// c:                4 Bytes
// total size:       16 Bytes
// total padding:    3 Bytes
// padding overhead: 19%

  • Punto a favor. He editado la pregunta con Bytes en lugar de sufijo B. En mi opinión, eliminar el sufijo por completo sería mucho más confuso y no correcto.

    – Dom324

    22 de diciembre de 2022 a las 1:05

  • Bueno, eso es una cuestión de opinión, supongo. sizeof siempre devuelve un valor en bytespor lo que cada vez que hablamos de tamaños de tipos de datos, relleno, etc., estamos siempre hablando de bytes.

    – arroz

    22 de diciembre de 2022 a las 1:06

  • Probablemente sea un remanente de estudiar ingeniería eléctrica en la uni, ver un número sin unidad al lado me parece un pecado

    – Dom324

    22 de diciembre de 2022 a las 1:32

  • ¿Qué sucede cuando creas una matriz de tu tipo? Considere un tipo que tiene 4 bytes int seguido de 1 byte char¿qué pasa si pones ese tipo en una matriz?

    – CoffeeTableEspresso

    22 de diciembre de 2022 a las 2:32

  • Tenga en cuenta que todas sus suposiciones de tamaño son para arquitectura de 64 bits (quizás x76_64 específicamente). 32 bits aún no está muerto y estará disponible durante bastantes años. Demonios, en algunos lugares se usan cosas de 16 bits. Dicho esto, ¿tal vez es una regla aquí simplemente asumir un procesador moderno de computadora portátil/escritorio/servidor? Lo que tu Realmente quiero mirar es el (algo maldito) packed atributo. Digo maldito, porque conduce a un acceso no alineado. Aunque se puede emparejar packed y aligned para especificar todas y cada una de las alineaciones que desee.

    – jaskij

    22 de diciembre de 2022 a las 21:08

avatar de usuario de dbush
arbusto

¿Hay alguna razón por la que no se aplica esta optimización?

Si esto fuera permitido, el valor de sizeof(struct B) sería ambiguo.

Supongamos que hiciste esto:

struct B b;
struct A a = { 1, 2 };
b.c = 0x12345678;
memcpy(&b.myStruct, &a, sizeof(struct A));

Estarías sobrescribiendo el valor de b.c.

  • Correcto, no pensé en el manejo manual de la memoria. Pero teóricamente, si el compilador pudiera probar que esto no sucederá, esta optimización sería segura, ¿no?

    – Dom324

    22 de diciembre de 2022 a las 1:16

  • El compilador no puede probar que esto no sucederá. Lea sobre el Problema de detención.

    – arroz

    22 de diciembre de 2022 a las 1:50

  • @Dom324 si struct a se define por sí mismo en un archivo, no hay forma de que el compilador sepa que struct b se definió en un archivo diferente no relacionado.

    – dbush

    22 de diciembre de 2022 a las 2:10

  • De hecho, recomendaría leer sobre teorema de arroz directamente en lugar del problema de la detención. el compilador puede a veces decide diseñar la estructura de manera diferente o no ponerla en la memoria, por ejemplo si no hay gestión de memoria manual y es de muy corta duración y no hay struct en absoluto. Sin embargo, uno no podrá detectar eso desde dentro del programa de ninguna manera debido a la regla “como si”.

    – yeputones

    22 de diciembre de 2022 a las 3:30


  • @yeputons: La optimización de la que está hablando es un caso de reemplazo escalar de agregados, SROA o SRA para abreviar. Realmente no está cambiando el diseño de la estructura, está optimizando la estructura por completo y simplemente trabajando con los miembros. GCC puede hacer esto incluso entre funciones, como se describe en el documento de 2010 El nuevo reemplazo escalar intraprocedimental de agregadoscomo el -fipa-sra opción de optimización. (Solo un éxito aleatorio de Google para “reemplazo escalar de agregados”). Los JIT de Java y Javascript también hacen esto.

    – Peter Cordes

    22 de diciembre de 2022 a las 4:17

El relleno se utiliza para forzar la alineación. Ahora, si tiene una matriz de struct myStruct, entonces hay una regla que establece que los elementos de la matriz se suceden sin ningún relleno. En su caso, sin el relleno dentro de myStruct después del último campo, el segundo myStruct en una matriz no estaría correctamente alineado. Por lo tanto, es necesario que sizeof(myStruct) sea un múltiplo de la alineación de myStruct, y para eso es posible que necesite suficiente relleno al final.

  • Sí, pero como puede ver, la estructura B no contiene ninguna matriz de la estructura A (myStructs), por lo que esto es irrelevante. Estoy hablando de un caso especial cuando tiene una estructura dentro de una estructura, y no es una matriz y, por lo tanto, no necesita el relleno final para forzar la alineación adecuada de la siguiente estructura.

    – Dom324

    24 de diciembre de 2022 a las 23:49

¿Ha sido útil esta solución?