Esta es la primera entrega de una serie en la que exploraremos cuestiones que se "escapan" del alto nivel de los lenguajes de programación hacia el hardware por debajo de nosotros.
¿Cuánto ocupa una struct, un registro? Pues... depende. Veamos a continuación un ejemplo en C.
typedef struct _Deportista {
unsigned int num;
unsigned int totalSeconds;
bool finished;
} Deportista;
Vale, un registro que identifica a un deportista con un número, un tiempo (en segundos), y un booleano que determina si terminó la carrera o no.
¿Cuál es el tamaño de este registro? De nuevo... depende. Podemos especificar un poco más:
- Ancho de palabra del ordenador.
- Compilador utilizado.
El ancho de palabra del ordenador nos permite saber dos cosas: primero, cuántos bytes ocupará un unsigned int, un entero sin signo. En lenguajes de programación cercanos al hardware, es muy común que el tipo int coincida con el ancho de palabra de la máquina. Si nuestro ordenador es de 32 bits, es decir, 4 bytes, el tamaño en memoria de un int será también de 4 bytes. Si nuestra máquina es de 64 bits, es decir, 8 bytes, el tamaño de un int será... bueno, pues depende del compilador. Puede optar por soportar enteros de 64 o 32 bits, aunque el puntero siempre sea de 64 bits. Por ejemplo, gcc y la mayoría de compiladores para x64 optan por seguir utilizando 4 bytes para los enteros.
Si asumimos una máquina de 32 bits, entonces tenemos 4 + 4 + 1, es decir, 9 bytes ocupados por el registro. Si asumimos una máquina de 64 bits, que utilice 32 bits para los enteros, tendremos el mismo resultado. Si asumimos una máquina x64 que utilice 8 bytes para los enteros, tendremos 8 + 8 + 1, es decir, 17 bytes. ¿No?
Pues lo más probable (y razonable), es que no sea así. El compilador tratará de optimizar el tamaño del registro para soportar el acceso a memoria en palabras, y con una alineación correcta.
Las siguientes asunciones y descripciones de funcionamiento se hacen todas eligiendo simplificar.
El bus de datos del ordenador lee o escribe de la memoria siempre con el mismo tamaño mínimo: el ancho de palabra de la arquitectura. Una máquina de 32 bits siempre lee o escribe de 32 en 32 bits, es decir, 4 bytes. Si por poner un ejemplo, lo que queremos son los 2 bytes intermedios, le aplicará posteriormente la máscara 0x00FFFF00 para obtener el resultado que le hemos pedido. Este es el acceso a memoria de palabra en palabra.
En nuestra estructura, los dos primeros miembros están alineados. Son dos accesos, uno por palabra. Supongamos por un momento la misma máquina de 32 bits, que el compilador reserva el tamaño de la estructura exacta, es decir, 9 bytes, y que tenemos un vector de dos deportistas, el número 1, que tardó 10 segundos, y el número 2, que tardó 9. Leemos el primer miembro del primer registro, y obtenemos 0x01000000, para leer el segundo miembro del primer registro, obteniendo 0x0A000000. Ahora leemos el tercer miembro, que es un booleano que ocupa 1 bytes, con el valor 1. Esto sería 0x01. Pero el ordenador no lee de byte en byte, sino de palabra en palabra. Así que lee 0x01020000. Esto es el byte de haber terminado la carrera del primer deportista, más los tres primeros bytes del número del segundo deportista. El ordenador aplica la máscara 0xFF000000, y nos devuelve 0x01. Ahora leemos al segundo miembro. Es la misma palabra de antes, 0x01020000, pero ahora aplicando la máscara 0x00FFFFFF. Pero esto no llega, nos falta un byte por leer, así que leemos la siguiente palabra y le aplicams la máscara 0xFF000000. Tendremos que desplazar los bits del primer valor hacia la izquierda, desplazar los bits del segundo valor hacia la derecha, y hacer un OR. ¡Cada miembro del segundo deportista implica lectura de dos palabras, desplazamientos de bits en los registros y sumas! Esto es así porque el registro no está alineado en memoria.
Ahora supongamos que el registro no ocupa 9 bytes (4 + 4 + 1), sino 12 bytes (4 + 4 + 4). Es decir, "malgastamos" 4 bytes en el booleano, solo para que el registro esté alineado en memoria. Así, la lectura de cada deportista se puede hacer mediante tres accesos simples a memoria, aunque los tres últimos bytes en realidad no se empleen, y estén siempre a cero. Lo que permite este "desperdicio" es que la velocidad se incremente enormemente. A este desperdicio se le denomina padding. Y el padding exacto depende de cada compilador, no está estandarizado.
El primer deportista se lee mediante dos accesos simples a memoria. El booleano se obtiene mediante un acceso simple y un enmascaramiento de 0xFF000000. El segundo deportista se lee mediante dos accesos simples a memoria, pues ya no hay desalineación, más un acceso simple con un enmascaramiento y así sucesivamente.
¡Ya sé! Puedo hacer esto:
typedef struct _Deportista {
unsigned int num;
unsigned int totalSeconds;
bool finished: 1;
} Deportista;
Le estamos diciendo que utilice un solo bit para almacenar el booleano. Genial, pero nada ha cambiado en realidad. Solo la operación de máscara. Si cambiara algo, volveríamos al problema de la desalineación en memoria.
Lo que sí podemos hacer es codificar con el acceso a memoria, y la alineación, en la cabeza. Por ejemplo:
typedef struct _Deportista {
unsigned int num;
bool finished;
unsigned int totalSeconds;
unsigned short int numPenalties;
} Deportista;
Esta estructura ocupa 4 + 4 + 4 + 4 = 16 bytes, a pesar de que en principio el campo finished solo ocupa 1 byte, y también que con el número de penalizaciones numPenalties, escogido como un entero corto sin signo, en nuestra arquitectura imaginaria es de solo 16 bits. Pero que por cuestiones de alineación en realidad, no conseguimos ahorrar nada; el compilador creará padding tras finished y tras numPenalties.
La cosa cambia si reordenamos los miembros de la estructura:
typedef struct _Deportista {
unsigned int num;
bool finished;
unsigned short int numPenalties;
unsigned int totalSeconds;
} Deportista;
Ahora, siguiendo los cálculos realizados anteriormente, nuestra estructura ocupa 4 + 4 + 4, es decir, 12 bytes. El compilador solo introducirá un byte de padding tras numPenalites. Tras el segundo acceso a memoria, finished se servirá aplicando la máscara 0xFF000000 al valor obtenido, mientras que numPenalites se obtendrá del mismo valor aplicando la máscara 0x00FFFF00.
Top comments (0)