Efecto #pragma pack

233

Me preguntaba si alguien podría explicarme qué hace la #pragma packdeclaración del preprocesador y, lo que es más importante, por qué uno querría usarla.

Revisé la página de MSDN , que ofrecía algunas ideas, pero esperaba escuchar más de personas con experiencia. Lo he visto en código antes, aunque parece que ya no puedo encontrar dónde.

Cenoc
fuente
1
Obliga a una alineación / empaque particular de una estructura, pero como todas las #pragmadirectivas, están definidas en la implementación.
dreamlax
A mod s = 0donde A es la dirección y s es el tamaño del tipo de datos; Esto comprueba si los datos no están desalineados.
legends2k

Respuestas:

422

#pragma packindica al compilador que empaque los miembros de la estructura con una alineación particular. La mayoría de los compiladores, cuando declara una estructura, insertará relleno entre los miembros para asegurarse de que estén alineados con las direcciones apropiadas en la memoria (generalmente un múltiplo del tamaño del tipo). Esto evita la penalización de rendimiento (o error absoluto) en algunas arquitecturas asociadas con el acceso a variables que no están alineadas correctamente. Por ejemplo, dados enteros de 4 bytes y la siguiente estructura:

struct Test
{
   char AA;
   int BB;
   char CC;
};

El compilador podría elegir colocar la estructura en la memoria de esta manera:

|   1   |   2   |   3   |   4   |  

| AA(1) | pad.................. |
| BB(1) | BB(2) | BB(3) | BB(4) | 
| CC(1) | pad.................. |

y sizeof(Test)sería 4 × 3 = 12, aunque solo contenga 6 bytes de datos. El caso de uso más común para #pragma(según mi conocimiento) es cuando se trabaja con dispositivos de hardware en los que debe asegurarse de que el compilador no inserte relleno en los datos y que cada miembro siga el anterior. Con #pragma pack(1), la estructura anterior se presentaría así:

|   1   |

| AA(1) |
| BB(1) |
| BB(2) |
| BB(3) |
| BB(4) |
| CC(1) |

Y sizeof(Test)sería 1 × 6 = 6.

Con #pragma pack(2), la estructura anterior se presentaría así:

|   1   |   2   | 

| AA(1) | pad.. |
| BB(1) | BB(2) |
| BB(3) | BB(4) |
| CC(1) | pad.. |

Y sizeof(Test)sería 2 × 4 = 8.

El orden de las variables en struct también es importante. Con variables ordenadas de la siguiente manera:

struct Test
{
   char AA;
   char CC;
   int BB;
};

y con #pragma pack(2), la estructura se presentaría así:

|   1   |   2   | 

| AA(1) | CC(1) |
| BB(1) | BB(2) |
| BB(3) | BB(4) |

y sizeOf(Test)sería 3 × 2 = 6.

Nick Meyer
fuente
76
Puede valer la pena agregar las desventajas del embalaje. (los accesos de objetos no alineados son lentos en el mejor de los casos, pero causarán errores en algunas plataformas)
jalf
11
Parece que las alineaciones "penalización de rendimiento" mencionadas en realidad podrían ser un beneficio en algunos sistemas danluu.com/3c-conflict .
66
@Pacerier No realmente. Esa publicación habla de una alineación bastante extrema (alineación en límites de 4KB). La CPU espera ciertas alineaciones mínimas para varios tipos de datos, pero estos requieren, en el peor de los casos, una alineación de 8 bytes (sin contar los tipos de vectores que pueden requerir una alineación de 16 o 32 bytes). No alinearse en esos límites generalmente le da un impacto notable en el rendimiento (porque una carga debe realizarse como dos operaciones en lugar de una), pero el tipo está bien alineado o no lo está. Una alineación más estricta que eso no le compra nada (y arruina la utilización de caché
jalf
66
En otras palabras, un doble espera estar en un límite de 8 bytes. Ponerlo en un límite de 7 bytes dañará el rendimiento. Pero ponerlo en un límite de 16, 32, 64 o 4096 bytes no le compra nada más de lo que el límite de 8 bytes ya le dio. Obtendrá el mismo rendimiento de la CPU, mientras que la utilización de la caché será mucho peor por los motivos descritos en esa publicación.
jalf
44
Por lo tanto, la lección no es "el empaque es beneficioso" (el empaque viola la alineación natural de los tipos, por lo que perjudica el rendimiento), sino simplemente "no se
sobrealinee
27

#pragmase usa para enviar mensajes no portátiles (como en este compilador solamente) al compilador. Cosas como deshabilitar ciertas advertencias y estructuras de embalaje son razones comunes. Deshabilitar advertencias específicas es particularmente útil si compila con las advertencias cuando el indicador de errores está activado.

#pragma packespecíficamente se usa para indicar que la estructura que se está empaquetando no debe tener sus miembros alineados. Es útil cuando tiene una interfaz asignada de memoria a una pieza de hardware y necesita poder controlar exactamente dónde apuntan los diferentes miembros de la estructura. En particular, no es una buena optimización de velocidad, ya que la mayoría de las máquinas son mucho más rápidas en el manejo de datos alineados.

nmichaels
fuente
17
Para deshacer después, haga esto: #pragma pack (push, 1) y #pragma pack (pop)
malhal
16

Le dice al compilador el límite para alinear objetos en una estructura. Por ejemplo, si tengo algo como:

struct foo { 
    char a;
    int b;
};

Con una típica máquina de 32 bits, que te normalmente "quiere" para tener 3 bytes de relleno entre ay bpara que baterrizarán en un límite de 4 bytes para maximizar su velocidad de acceso (y eso es lo que sucede normalmente por defecto).

Sin embargo, si tiene que coincidir con una estructura definida externamente, desea asegurarse de que el compilador presenta su estructura exactamente de acuerdo con esa definición externa. En este caso, se puede dar el compilador una #pragma pack(1)contarla no insertar ningún relleno entre los miembros - si la definición de la estructura incluye relleno entre los miembros, lo inserta de manera explícita (por ejemplo, por lo general con los miembros nombrados unusedNo ignoreN, o algo por el estilo orden).

Jerry Coffin
fuente
"normalmente" querría "tener 3 bytes de relleno entre a y b para que b aterrice en un límite de 4 bytes para maximizar su velocidad de acceso" - ¿cómo tener 3 bytes de relleno maximiza la velocidad de acceso?
Ashwin
8
@Ashwin: Colocar ben un límite de 4 bytes significa que el procesador puede cargarlo emitiendo una sola carga de 4 bytes. Aunque depende un poco del procesador, si está en un límite extraño, existe una buena posibilidad de que al cargarlo requiera que el procesador emita dos instrucciones de carga separadas, luego use una palanca de cambios para unir esas piezas. La penalización típica es del orden de una carga 3 veces más lenta de ese elemento.
Jerry Coffin
... si observa el código de ensamblaje para leer int alineado y no alineado, la lectura alineada suele ser un mnemotécnico único. La lectura no alineada puede ser de 10 líneas de ensamblaje fácilmente, ya que une el int, recogiéndolo byte a byte y colocándolo en las ubicaciones correctas del registro.
SF.
2
@SF .: Puede ser, pero incluso cuando no es así, no se deje engañar, en una CPU x86 (por ejemplo, obvio) las operaciones se llevan a cabo en hardware, pero aún así obtiene aproximadamente el mismo conjunto de operaciones y desaceleración.
Jerry Coffin
8

Los elementos de datos (por ejemplo, miembros de clases y estructuras) generalmente se alinean en los límites WORD o DWORD para los procesadores de generación actuales con el fin de mejorar los tiempos de acceso. Recuperar un DWORD en una dirección que no es divisible por 4 requiere al menos un ciclo de CPU adicional en un procesador de 32 bits. Entonces, si tiene, por ejemplo, tres miembros char char a, b, c;, en realidad tienden a ocupar 6 o 12 bytes de almacenamiento.

#pragmale permite anular esto para lograr un uso de espacio más eficiente, a expensas de la velocidad de acceso o para la consistencia de los datos almacenados entre diferentes objetivos del compilador. Me divertí mucho con esta transición del código de 16 bits a 32 bits; Espero que portar código de 64 bits cause el mismo tipo de dolores de cabeza para algunos códigos.

Pontus Gagge
fuente
En realidad, char a,b,c;generalmente tomará 3 o 4 bytes de almacenamiento (al menos en x86), eso se debe a que su requisito de alineación es de 1 byte. Si no fuera así, ¿cómo tratarías char str[] = "foo";? El acceso a a chares siempre una simple máscara de captación de desplazamiento, mientras que el acceso a una intpuede ser fetch-fetch-merge o simplemente fetch, dependiendo de si está alineado o no. inttiene (en x86) una alineación de 32 bits (4 bytes) porque de lo contrario obtendría (digamos) medio inten uno DWORDy medio en el otro, y eso requeriría dos búsquedas.
Tim Čas
3

El compilador podría alinear miembros en estructuras para lograr el máximo rendimiento en la plataforma determinada. #pragma packLa directiva le permite controlar esa alineación. Por lo general, debe dejarlo de forma predeterminada para un rendimiento óptimo. Si necesita pasar una estructura a la máquina remota, generalmente la usará #pragma pack 1para excluir cualquier alineación no deseada.

Kirill V. Lyadvinsky
fuente
2

Un compilador puede colocar miembros de la estructura en límites de bytes particulares por razones de rendimiento en una arquitectura particular. Esto puede dejar relleno no utilizado entre los miembros. El embalaje de estructura obliga a los miembros a ser contiguos.

Esto puede ser importante, por ejemplo, si necesita una estructura que se ajuste a un archivo en particular o un formato de comunicación donde los datos que necesita estén en posiciones específicas dentro de una secuencia. Sin embargo, dicho uso no se ocupa de los problemas de endianidad, por lo que, aunque se utiliza, puede no ser portátil.

También puede superponer exactamente la estructura de registro interno de algunos dispositivos de E / S, como un controlador UART o USB, por ejemplo, para que el acceso al registro sea a través de una estructura en lugar de direcciones directas.

Clifford
fuente
1

Es probable que solo desee usar esto si estuviera codificando en algún hardware (por ejemplo, un dispositivo mapeado en memoria) que tuviera requisitos estrictos para ordenar y alinear registros.

Sin embargo, esto parece una herramienta bastante contundente para lograr ese fin. Un mejor enfoque sería codificar un mini controlador en ensamblador y darle una interfaz de llamada C en lugar de buscar con este pragma.

msw
fuente
De hecho, lo uso bastante para ahorrar espacio en tablas grandes a las que no se accede con frecuencia. Allí, es solo para ahorrar espacio y no para una alineación estricta. (Solo te voté, por cierto. Alguien te había dado un voto negativo.)
Todd Lehman
1

Lo he usado en código antes, aunque solo para interactuar con código heredado. Esta era una aplicación Mac OS X Cocoa que necesitaba cargar archivos de preferencias de una versión anterior de Carbon (que en sí misma era compatible con la versión original de M68k System 6.5 ... ya se entiende). Los archivos de preferencias en la versión original eran un volcado binario de una estructura de configuración, que utilizaba el#pragma pack(1) para evitar ocupar espacio adicional y ahorrar basura (es decir, los bytes de relleno que de otro modo estarían en la estructura).

Los autores originales del código también habían usado #pragma pack(1)para almacenar estructuras que se usaban como mensajes en la comunicación entre procesos. Creo que la razón aquí fue evitar la posibilidad de tamaños de relleno desconocidos o modificados, ya que el código a veces miraba una parte específica de la estructura del mensaje contando una cantidad de bytes desde el principio (ewww).


fuente
1

He visto a personas usarlo para asegurarse de que una estructura tome una línea de caché completa para evitar el intercambio falso en un contexto multiproceso. Si va a tener una gran cantidad de objetos que se empaquetarán de manera holgada por defecto, podría ahorrar memoria y mejorar el rendimiento de la caché para empacarlos más, aunque el acceso a la memoria no alineado generalmente ralentizará las cosas, por lo que puede haber una desventaja.

metal de piedra
fuente
0

Tenga en cuenta que hay otras formas de lograr la consistencia de datos que ofrece #pragma pack (por ejemplo, algunas personas usan #pragma pack (1) para estructuras que deben enviarse a través de la red). Por ejemplo, vea el siguiente código y su salida posterior:

#include <stdio.h>

struct a {
    char one;
    char two[2];
    char eight[8];
    char four[4];
};

struct b { 
    char one;
    short two;
    long int eight;
    int four;
};

int main(int argc, char** argv) {
    struct a twoa[2] = {}; 
    struct b twob[2] = {}; 
    printf("sizeof(struct a): %i, sizeof(struct b): %i\n", sizeof(struct a), sizeof(struct b));
    printf("sizeof(twoa): %i, sizeof(twob): %i\n", sizeof(twoa), sizeof(twob));
}

El resultado es el siguiente: sizeof (struct a): 15, sizeof (struct b): 24 sizeof (twoa): 30, sizeof (twob): 48

Observe cómo el tamaño de la estructura a es exactamente el recuento de bytes, pero la estructura b tiene relleno agregado (consulte esto para obtener detalles sobre el relleno). Al hacer esto en lugar del paquete #pragma, puede tener el control de convertir el "formato de cable" en los tipos apropiados. Por ejemplo, "char two [2]" en un "short int", etc.

Wangchow
fuente
No, está mal. Si observa la posición en la memoria de b.two, no es un byte después de b.one (el compilador puede (y a menudo) alineará b.two para que esté alineado con el acceso de palabras). Para dos, es exactamente un byte después de uno. Si necesita acceder a a.two como un int corto, debe tener 2 alternativas, usar una unión (pero esto generalmente falla si tiene un problema de endianness) o desempaquetar / convertir por código (usando la función ntohX adecuada)
xryl669
1
sizeofdevuelve un size_tque debe imprimirse usando%zu . El uso del especificador de formato incorrecto invoca un comportamiento indefinido
phuclv
0

¿Por qué uno quiere usarlo?

Para reducir la memoria de la estructura.

¿Por qué uno no debería usarlo?

  1. Esto puede conducir a una penalización del rendimiento, porque algunos sistemas funcionan mejor en datos alineados
  2. Algunas máquinas no podrán leer los datos no alineados
  3. El código no es portátil
VINOTE ENERGÉTICO
fuente