¿Hay una forma estándar o una alternativa estándar para empacar una estructura en c?

13

Cuando se programa en CI, me resulta invaluable empacar estructuras utilizando el __attribute__((__packed__))atributo GCC para poder convertir fácilmente una porción estructurada de memoria volátil en una matriz de bytes que se transmitirán por un bus, se guardarán en el almacenamiento o se aplicarán a un bloque de registros. Las estructuras empaquetadas garantizan que, cuando se trata como una matriz de bytes, no contendrá ningún relleno, lo cual es un desperdicio, un posible riesgo de seguridad y posiblemente incompatible cuando se utiliza hardware de interfaz.

¿No hay un estándar para el embalaje de estructuras que funcione en todos los compiladores de C? Si no es así, ¿soy un caso atípico al pensar que esta es una característica crítica para la programación de sistemas? ¿Los primeros usuarios del lenguaje C no encontraron la necesidad de empacar estructuras o hay algún tipo de alternativa?

satur9nine
fuente
El uso de estructuras en los dominios de compilación es una muy mala idea, en particular para apuntar al hardware (que es otro dominio de compilación). Las estructuras de paquetes son solo un truco para hacer esto, tienen muchos efectos secundarios negativos, por lo que hay muchas otras soluciones a sus problemas con menos efectos secundarios, y que son más portátiles.
old_timer

Respuestas:

12

En una estructura, lo que importa es el desplazamiento de cada miembro de la dirección de cada instancia de estructura. No se trata tanto de cuán apretadas están las cosas.

Una matriz, sin embargo, importa cómo se "empaqueta". La regla en C es que cada elemento de la matriz es exactamente N bytes del anterior, donde N es el número de bytes utilizados para almacenar ese tipo.

Pero con una estructura, no existe tal necesidad de uniformidad.

Aquí hay un ejemplo de un esquema de empaque extraño:

Freescale (que fabrica microcontroladores automotrices) crea un micro que tiene un coprocesador de unidad de procesamiento de tiempo (google para eTPU o TPU). Tiene dos tamaños de datos nativos, 8 bits y 24 bits, y solo se ocupa de enteros.

Esta estructura:

struct a
{
  U24 elementA;
  U24 elementB;
};

verá que cada U24 almacena su propio bloque de 32 bits, pero solo en el área de dirección más alta.

Esta:

struct b
{
  U24 elementA;
  U24 elementB;
  U8  elementC;
};

tendrá dos U24s almacenada en adyacentes bloques de 32 bits, y la U8 será almacenada en el "agujero" en frente de la primera U24, elementA.

Pero puede decirle al compilador que empaque todo en su propio bloque de 32 bits, si lo desea; es más costoso en RAM pero usa menos instrucciones para acceder.

"empaquetar" no significa "empacar herméticamente", solo significa algún esquema para organizar los elementos de una estructura con el desplazamiento.

No hay un esquema genérico, depende del compilador + arquitectura.

RichColours
fuente
1
Si el compilador para el TPU se reorganiza struct bpara moverse elementCantes que cualquiera de los otros elementos, entonces no es un compilador de C conforme. El reordenamiento de elementos no está permitido en C
Bart van Ingen Schenau el
Interesante, pero U24 no es un tipo C estándar en.m.wikipedia.org/wiki/C_data_types, por lo que no es sorprendente que el cumplidor se vea obligado a manejarlo de una manera algo extraña.
satur9nine
Comparte RAM con el núcleo principal de la CPU que tiene un tamaño de palabra de 32 bits. Pero este procesador tiene una ALU que solo trata con 24 bits u 8 bits. Por lo tanto, tiene un esquema para diseñar números de 24 bits en palabras de 32 bits. No estándar, pero es un gran ejemplo de empaque y alineación. De acuerdo, es muy no estándar.
RichColours
6

Cuando la programación en CI ha encontrado invaluable empacar estructuras usando GCC __attribute__((__packed__))[...]

Como mencionó __attribute__((__packed__)), supongo que su intención es eliminar todo el relleno dentro de a struct(hacer que cada miembro tenga una alineación de 1 byte).

¿No hay un estándar para el embalaje de estructuras que funcione en todos los compiladores de C?

... y la respuesta es no". El relleno y la alineación de datos en relación con una estructura (y matrices contiguas de estructuras en la pila o el montón) existen por una razón importante. En muchas máquinas, el acceso no alineado a la memoria puede conducir a una penalización de rendimiento potencialmente significativa (aunque disminuyendo en algunos hardware más nuevos). En algunos casos excepcionales, el acceso desalineado a la memoria conduce a un error del bus que es irrecuperable (incluso puede bloquear todo el sistema operativo).

Dado que el estándar C se enfoca en la portabilidad, tiene poco sentido tener una forma estándar de eliminar todo el relleno en una estructura y simplemente permitir que los campos arbitrarios estén desalineados, ya que hacerlo podría potencialmente hacer que el código C no sea portátil.

La forma más segura y portátil de enviar dichos datos a una fuente externa de una manera que elimine todo el relleno es serializar a / desde secuencias de bytes en lugar de simplemente tratar de enviar los contenidos de memoria sin procesar de su structs. Eso también evita que su programa sufra penalizaciones de rendimiento fuera de este contexto de serialización, y también le permitirá agregar libremente nuevos campos a un structsin tirar y fallar todo el software. También le dará algo de espacio para abordar el endianness y cosas así si eso se convierte en una preocupación.

Hay una forma de eliminar todo el relleno sin llegar a las directivas específicas del compilador, aunque solo es aplicable si el orden relativo entre campos no importa. Dado algo como esto:

struct Foo
{
    double x;  // assume 8-byte alignment
    char y;    // assume 1-byte alignment
               // 7 bytes of padding for first field
};

... necesitamos el relleno para el acceso alineado a la memoria en relación con la dirección de la estructura que contiene estos campos, así:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______y.......x_______y.......x_______y.......x_______y.......

... donde .indica relleno. Todos xdeben alinearse con un límite de 8 bytes para el rendimiento (y, a veces, incluso el comportamiento correcto).

Puede eliminar el relleno de forma portátil utilizando una representación de SoA (estructura de matriz) como esta (supongamos que necesitamos 8 Fooinstancias):

struct Foos
{
   double x[8];
   char y[8];
};

Efectivamente hemos demolido la estructura. En este caso, la representación de la memoria se convierte así:

0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF
x_______x_______x_______x_______x_______x_______x_______x_______

... y esto:

01234567
yyyyyyyy

... no más sobrecarga de relleno, y sin involucrar el acceso a la memoria desalineada ya que ya no estamos accediendo a estos campos de datos como un desplazamiento de una dirección de estructura, sino como un desplazamiento de una dirección base para lo que efectivamente es una matriz.

Esto también conlleva la ventaja de ser más rápido para el acceso secuencial como resultado de menos datos para consumir (no más relleno irrelevante en la mezcla para ralentizar la tasa de consumo de datos relevante de la máquina) y también un potencial para que el compilador vectorice el procesamiento de manera muy trivial .

La desventaja es que es un código PITA. También es potencialmente menos eficiente para el acceso aleatorio con el paso más grande entre campos, donde a menudo los representantes de AoS o AoSoA lo harán mejor. Pero esa es una forma estándar de eliminar el relleno y empacar las cosas lo más apretado posible sin atornillar con la alineación de todo.

ChrisF
fuente
2
Yo diría que tiene un medio de especificación de diseño de la estructura de forma explícita sería masivamente mejorar la portabilidad. Si bien algunos diseños conducirían a un código muy eficiente en algunas máquinas y a un código muy ineficiente en otras, el código funcionaría en todas las máquinas y sería eficiente en al menos algunas. Por el contrario, en ausencia de tal característica, es probable que la única forma de hacer que el código funcione en todas las máquinas sea hacerlo ineficiente en todas las máquinas o bien usar un montón de macros y compilación condicional para combinar un rápido no portátil programa y un portátil lento en la misma fuente.
supercat
Conceptualmente sí, si pudiéramos especificar todo hasta representaciones de bits y bytes, requisitos de alineación, endianness, etc. y tener una característica que permita un control tan explícito en C mientras que opcionalmente lo separe más de la arquitectura subyacente ... Pero solo estaba hablando de ATM: actualmente, la solución más portátil para un serializador es escribirlo de tal manera que no dependa de las representaciones exactas de bits y bytes y la alineación de los tipos de datos. Desafortunadamente, carecemos de los medios ATM para hacer lo contrario de manera efectiva (en C).
5

No todas las arquitecturas son iguales, solo active la opción de 32 bits en un módulo y vea qué sucede cuando se usa el mismo código fuente y el mismo compilador. El orden de los bytes es otra limitación bien conocida. Agregue la representación de coma flotante y los problemas empeorarán. Usar el embalaje para enviar datos binarios no es portátil. Para estandarizarlo de modo que sea prácticamente utilizable, deberá redefinir la especificación del lenguaje C.

Aunque es común, usar Pack para enviar datos binarios es una mala idea si desea seguridad, portabilidad o longevidad de los datos. ¿Con qué frecuencia lees un blob binario de una fuente en tu programa? ¿Con qué frecuencia verificas que todos los valores son razonables, que un hacker o un cambio de programa no han "llegado" a los datos? Para cuando haya codificado una rutina de verificación, bien podría estar utilizando rutinas de importación y exportación.

Mattnz
fuente
0

Una alternativa muy común es el "relleno con nombre":

struct s {
  short s1;
  char  c2;
  char  reserved; // Padding
};

Esto hace suponer que la estructura no se rellenará a 8 bytes.

MSalters
fuente