Layout de memoria: sizeof, alignof y padding

Cuando defines un tipo en C++, no solo estás creando un modelo lógico, sino una disposición física en la memoria. El operador sizeof devuelve el tamaño en bytes de un tipo o una expresión, mientras que alignof nos indica la alineación necesaria (en bytes) para que ese tipo sea accedido de forma eficiente por el hardware.

Para entender esto, debemos aceptar que la CPU no lee la memoria byte a byte, sino en bloques (palabras de 4, 8 o más bytes). Si un int de 4 bytes empieza en una dirección de memoria que no es múltiplo de 4, el procesador podría necesitar dos ciclos de lectura de memoria en lugar de uno, o incluso generar una excepción de alineación en arquitecturas estrictas. Por ello, el compilador inserta padding (relleno): bytes invisibles entre miembros de un struct para asegurar que cada uno comience en una dirección válida según su alignof.

sizeof es un operador que se evalúa en tiempo de compilación y no tiene efectos secundarios; evaluar sizeof(f()) no ejecutará la función f(). En cambio, alignof responde a los requisitos de alineación natural de la arquitectura. Debes usar sizeof cuando necesites dimensionar buffers o realizar serialización, y alignof cuando estés escribiendo allocators personalizados o código que interactúe con hardware/SIMD. Si intentas calcular manualmente el tamaño de una estructura sumando sus miembros sin considerar el padding, romperás la integridad de los datos y causarás un comportamiento indefinido al acceder a los miembros.

#include <iostream>
#include <cstddef>   // std::offsetof
#include <type_traits>

// Estructura con layout ineficiente debido al padding
struct Ineficiente {
    char a;             // 1 byte
    // 3 bytes de padding aquí para alinear el siguiente int a 4 bytes
    int b;              // 4 bytes
    char c;             // 1 byte
    // 3 bytes de padding al final para que el sizeof sea múltiplo de 4
};

// Estructura optimizada mediante reordenamiento de miembros
struct Eficiente {
    int b;              // 4 bytes
    char a;             // 1 byte
    char c;             // 1 byte
    // 2 bytes de padding al final para completar el múltiplo de 4
};

// Uso de alignas para forzar alineación de línea de caché (típico 64 bytes)
struct alignas(64) CacheAligned {
    int x;
};

// Template con parameter packs para calcular el tamaño total de un pack de tipos
template <typename... Ts>
constexpr std::size_t suma_tamaños_pack() {
    return (sizeof(Ts) + ...); // Fold expression [C++17]
}

int main() {
    // Verificación de tamaños y alineación
    std::cout << "--- Layout de Memoria ---\n";
    std::cout << "sizeof(Ineficiente): " << sizeof(Ineficiente) << " bytes\n";
    std::cout << "alignof(Ineficiente): " << alignof(Ineficiente) << "\n";
    std::cout << "offsetof(Ineficiente, b): " << offsetof(Ineficiente, b) << "\n\n";

    std::cout << "sizeof(Eficiente): " << sizeof(Eficiente) << " bytes\n";
    std::cout << "sizeof(CacheAligned): " << sizeof(CacheAligned) << " bytes\n";
    std::cout << "alignof(CacheAligned): " << alignof(CacheAligned) << "\n\n";

    // Uso de std::max_align_t para obtener la alineación máxima fundamental
    std::cout << "max_align_t: " << alignof(std::max_align_t) << " bytes\n";

    // Uso de sizeof... en un context de template
    constexpr std::size_t total = suma_tamaños_pack<char, int, double>();
    std::cout << "Suma teórica (sin padding) de char, int, double: " << total << " bytes\n";

    return 0;
}

Desglose del diseño

En la estructura Ineficiente, fíjate en cómo sizeof(Ineficiente) resulta en 12 bytes, a pesar de que la suma de sus miembros (1 + 4 + 1) es 6. Esto ocurre porque el compilador inserta 3 bytes de padding tras a para que b comience en una dirección múltiplo de 4, y otros 2 o 3 bytes al final para que el tamaño total de la estructura sea múltiplo de su alineación máxima (4), permitiendo que un array de estas estructuras mantenga la alineación en cada elemento.

Al reordenar los miembros en Eficiente, colocamos el tipo de mayor alineación (int b) al principio. Esto permite que a y c ocupen sus posiciones consecutivas y que el padding se reduzca al mínimo necesario al final de la estructura, bajando el sizeof de 12 a 8 bytes. Este es un patrón crítico en sistemas de alto rendimiento para mejorar la localidad de caché y reducir el uso de memoria.

En el template suma_tamaños_pack, utilizamos una fold expression [C++17] con sizeof.... Es vital distinguir que sizeof...(Ts) devuelve la cantidad de tipos en el pack, mientras que (sizeof(Ts) + ...) expande la operación para sumar los tamaños de cada uno. Nota que el resultado de suma_tamaños_pack<char, int, double>() será 13, lo cual es la suma pura, sin tener en cuenta el padding que el compilador añadiría si esos tipos estuvieran en una struct.

Finalmente, alignas(64) sobre CacheAligned fuerza al compilador a que cada instancia de este objeto comience en una dirección múltiplo de 64. Esto es fundamental en programación de bajo nivel para evitar el false sharing en sistemas multihilo, asegurando que el objeto ocupe su propia línea de caché.

El error frecuente

Un error clásico ocurre al intentar serializar datos o copiar memoria manualmente asumiendo que el tamaño de una struct es la suma de sus miembros.

struct ErrorStruct {
    char a;
    int b;
}; // sizeof es 8, no 5

char buffer[10];
ErrorStruct s = {'x', 100};
// ERROR: Estamos copiando 5 bytes, pero la estructura real ocupa 8.
// El siguiente objeto en un array de ErrorStruct quedaría desfasado.
std::memcpy(buffer, &s, sizeof(s)); 

Si intentas leer esta estructura desde un buffer de red o un archivo usando un puntero y asumes que b está en el offset 1, estarás leyendo basura o fallando en sistemas con alineación estricta. Siempre usa offsetof o, mejor aún, evita el casting de punteros de tipos POD (Plain Old Data) y utiliza serialización robusta si los datos provienen de un entorno externo. Para detectar estos problemas de alineación o desbordamientos en el desarrollo, utiliza siempre -fsanitize=alignment en Clang/GCC.

14

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio