Publi

¡ Siempre olvido la alineación de las variables ! GRR

1GB DDR3 Memory Module

Algo que pocas veces tenemos en cuenta es la alineación de las variables en la memoria RAM. Muchas veces, ni nos va, ni nos viene, aunque en ciertas ocasiones suele causar calentamientos de cabeza.

Tiene que ver con la forma que tiene la CPU para dialogar con la RAM y la arquitectura de éstas. Partimos del hecho de que pedir un dato a la memoria es algo lento, sí se hace muchos millones de veces por segundo, pero mientras viene o no viene el dato, la CPU simplemente espera.  Ahora, tenemos que tener en cuenta:

  • El diálogo CPU<–>Memoria es a través de palabras y la palabra, en un sistema de 32bit es de 32bit (4bytes), en uno de 64bit serán 8bytes, no en todos los procesadores es así, pero casi.
  • Cada byte en memoria tiene una dirección, para que la CPU y la memoria puedan ubicar el dato.
  • Por otra parte,  las palabras en memoria tienen una posición que coincide con que su dirección sea divisible por la longitud de la palabra.
  • Sólo podemos acceder a una palabra en un mismo instante.

Por tanto, en mi PC de 32bit, cuando quiero un int de 32bit, normalmente me lo traigo de direcciones que sean divisibles por 4, por lo tanto, sólo hago una lectura a la memoria; por ejemplo, mi dato ocupa desde la dirección 0x0030 a la 0x0033 (4bytes), me lo traigo de una sola tirada. De otro modo, si mi dato ocupara desde la direccion 0x0032 a la 0x0035 (4bytes también), tendría que hacer dos peticiones, la palabra 0x0030–0x0033 y la palabra 0x0034-0x0036, y luego coger los dos últimos bytes de una y los dos últimos bytes de la otra; cosa que por experimentar está bien, pero no es óptimo, perdemos mucho tiempo, tardamos el doble. Si nos queremos traer una variable de 8bytes, tendremos que hacerlo de dos tandas, y por diseño, es más fácil traérsela de una dirección de memoria divisible por 8

También tenemos que tener en cuenta que perdemos memoria, es decir, tendremos bytes en memoria que no usaremos para nada (y a veces no nos sobra precisamente), pero sólo podemos aguantarnos… u optimizar más nuestros programas (siempre que compense). Por ejemplo cuando declaramos dos variables, un short (2bytes) y un int(4 bytes), la primera de ellas (el short) se situará en memoria en la dirección 0x0030 y la segunda (el int) en la 0x0034, dejando 2 bytes dentro de la memoria inutilizados. Aunque los compiladores modernos, a la hora de declarar variables intentan hacerlo de la forma más inteligente posible, para aprovechar los huecos al máximo, aunque no declaremos las variables por orden, a la hora de situarlas en memoria, se suelen colocar bien:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>

int main()
{
  char c,c2,c3,c4,c5,c6;
  int i;
  short k;
  long long i2;
  long long ai2;
  short k2;


  printf("%lX [c]\t\t(%lu bytes)\n",    (long unsigned)&c,   sizeof(c));
  printf("%lX [c2]\t\t(%lu bytes)\n",   (long unsigned)&c2,  sizeof(c2));
  printf("%lX [c3]\t\t(%lu bytes)\n",   (long unsigned)&c3,  sizeof(c3));
  printf("%lX [c4]\t\t(%lu bytes)\n",   (long unsigned)&c4,  sizeof(c4));
  printf("%lX [c5]\t\t(%lu bytes)\n",   (long unsigned)&c5,  sizeof(c5));
  printf("%lX [c6]\t\t(%lu bytes)\n",   (long unsigned)&c6,  sizeof(c6));
  printf("%lX [i]\t\t(%lu bytes)\n",    (long unsigned)&i,   sizeof(i));
  printf("%lX [i2]\t\t(%lu bytes)\n",   (long unsigned)&i2,  sizeof(i2));
  printf("%lX [k]\t\t(%lu bytes)\n",    (long unsigned)&k,   sizeof(k));
  printf("%lX [ai2]\t\t(%lu bytes)\n",  (long unsigned)&ai2, sizeof(ai2));
  printf("%lX [k2]\t\t(%lu bytes)\n",   (long unsigned)&k2,  sizeof(k2));

  return 0;
}

Nota: represento variables de tipo long unsigned para representar bien los valores en sistemas de 64bit.
El resultado es algo así:

BFAF2BCF [c]            (1 bytes)
BFAF2BCE [c2]           (1 bytes)
BFAF2BCD [c3]           (1 bytes)
BFAF2BCC [c4]           (1 bytes)
BFAF2BCB [c5]           (1 bytes)
BFAF2BCA [c6]           (1 bytes)
BFAF2BC4 [i]            (4 bytes)
BFAF2BB8 [i2]           (8 bytes)
BFAF2BC2 [k]            (2 bytes)
BFAF2BB0 [ai2]          (8 bytes)
BFAF2BAE [k2]           (2 bytes)

Esto resultará lo siguiente:

BFAF2BA8BFAF2BABBFAF2BACk2k2BFAF2BAF
BFAF2BB0ai2ai2ai2ai2BFAF2BB3BFAF2BB4ai2ai2ai2ai2BFAF2BB7BFAF2BB8i2i2i2i2BFAF2BB7BFAF2BBCi2i2i2i2BFAF2BBFBFAF2BC0kkBFAF2BC3BFAF2BC4iiiiBFAF2BC7BFAF2BC8c6c5BFAF2BC7BFAF2BCCc4c3c2c1BFAF2BCF

El caso es que las variables siempre encajan.

Pero el tema es que cuando creamos un struct en C, lógicamente las variables también tienen que encajar, y además tienen que estar en el mismo orden en que las ponemos, el compilador, esta vez no tiene la libertad de antes de reubicar información.
Vemos el siguiente ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>

typedef struct
{
  short s1;         /* 2 */
  int   i1;         /* 4 */
  long long l1;         /* 8 */
  int   i2;         /* 4 */
  long long l2;         /* 8 */
  short s2;         /* 2 */
  char  c1;         /* 1 */
  short s3;         /* 2 */
} mi_struct;

int main()
{
  mi_struct e1;
  printf("%lX [e1]\t\t(%lu bytes)\n",       (long unsigned)&e1,      sizeof(e1));
  printf("%lX [e1.s1]\t\t(%lu bytes)\n",    (long unsigned)&e1.s1,   sizeof(e1.s1));
  printf("%lX [e1.i1]\t\t(%lu bytes)\n",    (long unsigned)&e1.i1,   sizeof(e1.i1));
  printf("%lX [e1.l1]\t\t(%lu bytes)\n",    (long unsigned)&e1.l1,   sizeof(e1.l1));
  printf("%lX [e1.i2]\t\t(%lu bytes)\n",    (long unsigned)&e1.i2,   sizeof(e1.i2));
  printf("%lX [e1.l2]\t\t(%lu bytes)\n",    (long unsigned)&e1.l2,   sizeof(e1.l2));
  printf("%lX [e1.s2]\t\t(%lu bytes)\n",    (long unsigned)&e1.s2,   sizeof(e1.s2));
  printf("%lX [e1.c1]\t\t(%lu bytes)\n",    (long unsigned)&e1.c1,   sizeof(e1.c1));
  printf("%lX [e1.s3]\t\t(%lu bytes)\n",    (long unsigned)&e1.s3,   sizeof(e1.s3));
 
  return 0;
}

Y en este ejemplo sí que podemos ver gran diferencia entre 32bit y 64bit.

32bit
BFC5E510 [e1]           (36 bytes)
BFC5E510 [e1.s1]                (2 bytes)
BFC5E514 [e1.i1]                (4 bytes)
BFC5E518 [e1.l1]                (8 bytes)
BFC5E520 [e1.i2]                (4 bytes)
BFC5E524 [e1.l2]                (8 bytes)
BFC5E52C [e1.s2]                (2 bytes)
BFC5E52E [e1.c1]                (1 bytes)
BFC5E530 [e1.s3]                (2 bytes)
BFC5E510e1.s1e1.s1??BFC5E513BFC5E514e1.i1e1.i1e1.i1e1.i1BFC5E517
BFC5E518e1.l1e1.l1e1.l1e1.l1BFC5E51BBFC5E51Ce1.l1e1.l1e1.l1e1.l1BFC5E51F
BFC5E520e1.i2e1.i2e1.i2e1.i2BFC5E523BFC5E524e1.l2e1.l2e1.l2e1.l2BFC5E527
BFC5E528e1.l2e1.l2e1.l2e1.l2BFC5E52BBFC5E52Ce1.s2e1.s2e1.c1?BFC5E52F
BFC5E530e1.s3e1.s3??BFC5E533

Ya de primeras vemos que la estructura ocupa 36bytes y la suma de sus partes (2 + 4 + 8 + 4 + 8 + 2 + 1 + 2 = 31 bytes), por lo que vemos que la estructura ocupa más.
Si vemos el mapa de memoria (la tabla de arriba), vemos cinco interrogaciones, es decir, 5 huecos donde no trabajamos, y sólo servirán para poder alinear los demás datos.
Vemos que las primeras interrogaciones están en BFC5E512 y es que lo próximo que tenemos que colocar es un entero, por ello nos desplazamos hasta BFC5E514. El siguiente byte problemático es justo después de un char, y lo siguiente será al final, porque se nos quedan algunos bytes “colgados” y no vamos a poder aprovechar.

64bit
7FFFB3652EF0 [e1]               (40 bytes)
7FFFB3652EF0 [e1.s1]            (2 bytes)
7FFFB3652EF4 [e1.i1]            (4 bytes)
7FFFB3652EF8 [e1.l1]            (8 bytes)
7FFFB3652F00 [e1.i2]            (4 bytes)
7FFFB3652F08 [e1.l2]            (8 bytes)
7FFFB3652F10 [e1.s2]            (2 bytes)
7FFFB3652F12 [e1.c1]            (1 bytes)
7FFFB3652F14 [e1.s3]            (2 bytes)

En la tabla acortaré las direcciones de memoria:

B3652EF0e1.s1e1.s1??e1.i1e1.i1e1.i1e1.i1B3652EF7
B3652EF8e1.l1e1.l1e1.l1e1.l1e1.l1e1.l1e1.l1e1.l1B3652EFF
B3652F00e1.i2e1.i2e1.i1e1.i2????B3652F07
B3652F08e1.l2e1.l2e1.l2e1.l2e1.l2e1.l2e1.l2e1.l2B3652F0F
B3652F10e1.s2e1.s2e1.c1?e1.s3e1.s3??B3652F17

De primeras vemos que en 64bit la estructura ocupa 40bytes en lugar de 31bytes… pero esta vez, vemos cómo a la hora de colocar un long int (8bytes) en B3652F04, se lo lleva a B3652F08, así será capaz de leerlo en una lectura a memoria (de la palabra entera de 8bytes) en lugar de 2 lecturas (de 4bytes cada una en dos palabras diferentes).

¿Cuándo debemos tener esto en cuenta?

Esto tampoco tiene mucha importancia si sólo vamos a almacenar una pequeña información en un registro, pero, si vamos a almacenar un array de muchos elementos de un registro, o una lista enlazada, podremos observar que si en el registro tenemos 9bytes que sólo sirven para hacer bulto, es decir, para conseguir alinear la información, en un millón de elementos, estaremos perdiendo cerca de 9Mb, nos puede ayudar a optimizar un poco la información que almacenamos.

Por otra parte, si lo que estamos haciendo es implementar una lectura/escritura de información en un formato determinado y, por ejemplo, tenemos:

  • 3 bytes de identificación
  • 4 bytes de control
  • 4 bytes tamaño
  • resto de archivo de información

No podremos crear un registro del tipo:

1
2
3
4
5
6
struct
{
  char identif[3];
  int  control;
  int  size;
} TFormato;

Ya que desde el último carácter hasta el entero de control habrá 1byte de diferencia y el formato no se respetará correctamente, por lo que seguramente haya algún fallo.

Esto tampoco quiere decir que no se pueda leer una variable que no esté alineada, siempre podemos hacerlo jugando con los bytes y trabajando un poco con el código, aunque las arquitecturas modernas lo permiten directamente con instrucciones especiales, eso sí, no se recomienda mucho su uso, ya que es más lento hacer dos peticiones a memoria, y luego trabajar con el resultado de las dos para unirlo; por otra parte, si trabajamos con un procesador que no disponga de estas instrucciones, tendremos que programar cómo hacemos las peticiones y la fusión de la información, aunque de eso se encargue el compilador, debemos tenerlo en cuenta si queremos que nuestro programa sea rápido.

Un pequeño ejemplo de un acceso no alineado

Si queremos provocar un acceso no alineado en C, podemos hacer lo siguiente: crear un array de char más o menos grande, y un puntero a un entero que apunte a una dirección dentro del array de char (con que apunte a la dirección base+1 vale para que no esté alineado.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
  unsigned char datos[6];
  int *punt;
  printf("%lX (%lu)\n", (unsigned long)&datos, sizeof(datos));
  punt=(int*)&datos[1];
  printf("%lX (%lu)\n", (unsigned long)punt, sizeof(*punt));
  *punt=123456789;
  printf("%d %d %d %d => %d %X <= %X %X %X %X\n", datos[1], datos[2], datos[3], datos[4], *punt, *punt, datos[4], datos[3], datos[2], datos[1]);
 
}

La salida es algo así (en 64bits, aunque sólo se nota en el tamaño de las direcciones de memoria):

7FFFC98BE8B0 (6)
7FFFC98BE8B1 (4)
21 205 91 7 => 123456789 75BCD15 <= 7 5B CD 15

Primero mostramos la dirección de memoria base del array y el tamaño del mismo, luego la dirección de memoria base de la variable entera con la que trabajaremos y su tamaño (muy importante el sizeof(*punt), sin el * nos daría el tamaño del puntero, y estamos en 64bit, es decir, sería de 8bytes.
Y tras ello, asignaremos al entero el valor 123456789, podremos ver el valor en decimal de cada uno de los bytes que componen el entero, el valor del entero, y luego el valor del mismo en hexadecimal acompañado de los valores en hexadecimal de cada uno de los bytes de antes.

Esto está diseñado para verse en un sistema little endian, en un sistema big endian, la última línea de salida sería:

7 91 205 21 => 123456789 75BCD15 <= 15 CD 5B 7

Foto: wwarby (Flickr)

También podría interesarte...

There are 4 comments left Ir a comentario

  1. Pingback: BlogESfera.com /

  2. kenkeiras /
    Usando Mozilla Firefox Mozilla Firefox 4.0 en Linux Linux

    Está muy interesante, no pensaba que ocurriera esto con lenguajes de alto nivel como C ( suponía que da alguna forma lo “allanaban” )

  3. Gaspar Fernández /
    Usando Mozilla Firefox Mozilla Firefox 4.0 en Linux Linux

    Y está allanado, aquí sobre todo lo cuento desde el punto de vista de la optimización y la lectura de formatos de archivo. Es un hecho que tenemos que tener muy en cuenta en algunos ensambladores, y normalmente cuando programamos en cualquier lenguaje no nos afecta, pero si queremos cuidar nuestro código y optimizarlo es algo que deberíamos tener en cuenta.

  4. Pingback: Poesía binaria » Leyendo archivos de imagen en formato BMP en C /

Leave a Reply