O que é?

O alinhamento de estruturas de dados é a forma que um dado é organizado e acessado na memória do computador. E pode ser organizado em três temas:

  • Data alignment - alinhamento de dados
  • Data structure padding - preenchimento da estrutura de dados
  • Data structure packing - empacotamento da estrutura de dados

Alinhamento

Na maioria dos cenários, você nunca precisa se preocupar com o alinhamento porque o alinhamento padrão já é ideal. Mas ao trabalhar com baixo nível e sistemas embarcados, frequentemente irá se deparar com este assunto.

Função alignof

Para obter o alinhamento, em bytes, o operador alignof() pode ser usado.

A seguinte estrutura possui um alinhamento de 4 bytes e será explicado na sessão Preenchimento.

#include <iostream>

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

int main()
{
    struct TestStructure test;
    
    std::cout << "Size of TestStructure alignment: " << alignof(test) << std::endl;
}

Função alignas

Para especificar o alinhamento de uma estrutura o especificador alignas() pode ser usado.

#include <iostream>

struct alignas(8) MyAlignedStructure {};

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

int main()
{
    struct TestStructure test;
    struct MyAlignedStructure testAlignedAs;
    
    std::cout << "Size of TestStructure alignment: " << alignof(test) << std::endl;
    std::cout << "Size of MyAlignedStructure alignment: " << alignof(testAlignedAs) << std::endl;
}

Resultado:

Size of TestStructure alignment: 4
Size of MyAlignedStructure alignment: 8

Quando múltiplos alignas são aplicados à mesma declaração, aquele com o maior valor é usado. Um alignas valor de 0 é ignorado.

#include <iostream>

struct alignas(8) alignas(16) MyAlignedStructure {};

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

union alignas(0) U1
{
    int i;
    float f;
};

union U2
{
    int i;
    float f;
};

int main()
{
    struct TestStructure test;
    struct MyAlignedStructure testAlignedAs;
    union U1 firstUnion;
    union U2 secondUnion;
    
    std::cout << "Size of TestStructure alignment: " << alignof(test) << std::endl;
    std::cout << "Size of MyAlignedStructure alignment: " << alignof(testAlignedAs) << std::endl;
    std::cout << "Size of firstUnion alignment: " << alignof(firstUnion) << std::endl;
    std::cout << "Size of secondUnion alignment: " << alignof(secondUnion) << std::endl;
}

Resultado:

Size of TestStructure alignment: 4
Size of MyAlignedStructure alignment: 16
Size of firstUnion alignment: 4
Size of secondUnion alignment: 4

Você pode fornecer um tipo como o valor de alinhamento. O alinhamento padrão do tipo é usado como o valor de alinhamento.

#include <iostream>

union alignas(long) U1
{
    int i;
    float f;
};

union U2
{
    int i;
    float f;
};

int main()
{
    union U1 firstUnion;
    union U2 secondUnion;
    
    std::cout << "Size of firstUnion alignment: " << alignof(firstUnion) << std::endl;
    std::cout << "Size of secondUnion alignment: " << alignof(secondUnion) << std::endl;
}

Resultado:

Size of firstUnion alignment: 8
Size of secondUnion alignment: 4

Um pacote de parâmetros de modelo (alignas (pack...)) pode ser usado para o valor de alinhamento. O maior valor de alinhamento de todos os elementos da embalagem é usado.

#include <iostream>

template <typename... Ts>
class alignas(Ts...) C2
{
    char c;
};


int main()
{
    C2<> c1;
    C2<short, int> c4;
    C2<int, float, double> c8;
    
    std::cout << "Size of c1 alignment: " << alignof(c1) << std::endl;
    std::cout << "Size of c4 alignment: " << alignof(c4) << std::endl;
    std::cout << "Size of c8 alignment: " << alignof(c8) << std::endl;

    return 0;
}

Resultado:

Size of c1 alignment: 1
Size of c4 alignment: 4
Size of c8 alignment: 8

Preenchimento

A maioria dos compiladores, quando você declara um struct, insere preenchimento entre os membros para garantir que eles sejam alinhados aos endereços apropriados na memória (geralmente um múltiplo do tamanho do tipo). Isso evita a penalidade de desempenho (ou erro total) em algumas arquiteturas associadas ao acesso a variáveis que não estão alinhadas corretamente.

No exemplo abaixo, temos duas variáveis do tipo char, AA e CC, e uma do tipo int, nomeada BB.

#include <iostream>

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

int main()
{
    struct TestStructure test;
    
    std::cout << "Size of char:" << " " << sizeof(test.AA) << std::endl;
    std::cout << "Size of int:" << " " << sizeof(test.BB) << std::endl;
    std::cout << "Size of char:" << " " << sizeof(test.CC) << std::endl;
}

O tamanho individual de cada variável foi obitda com a função sizeof() e a seguinte saída:

Size of char: 1
Size of int: 4
Size of char: 1

Qual é o tamanho da struct?

Intuitivamente falamos que seria a soma das variáveis da estrutura, ou seja, 6 bytes.

Porém, essa afirmação está errada e usando a função sizeof(), verificamos que o tamanho da estrutura é de 12 bytes!

Isso ocorre por causa do preenchimento (padding), conforme ilustrado abaixo:

|   1   |   2   |   3   |   4   |
| AA(1) | pad.................. |  // 1 byte + 3 bytes pad
| BB(1) | BB(2) | BB(3) | BB(4) |  // 4 bytes
| CC(1) | pad.................. |  // 1 byte + 3 bytes pad

A variável AA ocupa 4 bytes, sendo 1 byte a variável e 3 bytes de preenchimento.

#pragma pack

O #pragma pack instrui o compilador a empacotar os membros da estrutura com um alinhamento específico. A maioria dos compiladores, quando você declara um struct, insere preenchimento entre os membros para garantir que eles sejam alinhados aos endereços apropriados na memória (geralmente um múltiplo do tamanho do tipo de dado).

Então ao especificar #pragma pack(1), o alinhamento é de 1 byte para todos os membros do escopo daquela definição. Menos para as struturas que forem sobrescritas com alignas() ou outra forma de alterar o alinhamento.

#include <iostream>
#pragma pack(1)
struct alignas(2)  MyAlignedStructure {};

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

int main()
{
    struct TestStructure test;
    struct MyAlignedStructure testAlignedAs;
    
    std::cout << "Size of TestStructure alignment: " << alignof(test) << std::endl;
    std::cout << "Size of MyAlignedStructure alignment: " << alignof(testAlignedAs) << std::endl;
    
    std::cout << "Size of testAlignedAs struct:" << " " << sizeof(testAlignedAs) << std::endl;
    std::cout << "Size of test struct:" << " " << sizeof(test) << std::endl;
}

Resultado:

Size of TestStructure alignment: 1
Size of MyAlignedStructure alignment: 2
Size of testAlignedAs struct: 2
Size of test struct: 6

Então com o alinhamento de 1 byte, o tamanho da estrutura é a soma de seus membros.

E ao utilizar um alinhamento de 2 bytes?

#include <iostream>
#pragma pack(2)
struct alignas(2)  MyAlignedStructure {};

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

int main()
{
    struct TestStructure test;
    struct MyAlignedStructure testAlignedAs;
    
    std::cout << "Size of TestStructure alignment: " << alignof(test) << std::endl;
    std::cout << "Size of MyAlignedStructure alignment: " << alignof(testAlignedAs) << std::endl;
    
    std::cout << "Size of testAlignedAs struct:" << " " << sizeof(testAlignedAs) << std::endl;
    std::cout << "Size of test struct:" << " " << sizeof(test) << std::endl;
}

Resultado:

Size of TestStructure alignment: 2
Size of MyAlignedStructure alignment: 2
Size of testAlignedAs struct: 2
Size of test struct: 8

Então a estrutura é empacotada da seguinte forma:

|   1   |   2   |
| AA(1) | pad.. |  // 1 byte + 1 byte pad
| BB(1) | BB(2) |  // 2 bytes
| BB(3) | BB(4) |  // 2 bytes
| CC(1) | pad.. |  // 1 byte + 1 byte pad

Problema

O artigo “Anybody who writes #pragma pack(1) may as well just wear a sign on their forehead that says “I hate RISC”” explica os riscos de usar o #pragma pack(1).

Isso é verdade em sistemas desktop, porém em sistemas embarcados, nem tanto…

Em muitos casos o gargalo não está no processamento, mas em outros locais, como a comunicação. Então a perda de desempenho na otimização do processamento se torna aceitável, ao ganhar mais espaço para criar um pacote de dados e evitar os preenchimentos, ou até mesmo para dar compatibilidade entre dispositivos diferentes.

Portanto, cada caso deve ser analisado e não existem dogmas.

Opinião

Não utilizar as diretivas de compilador #pragma, pois ela é dependente do compilador utilizado. A maioria dos compiladores possuem suporte e um comportamento similar, mas em raros casos isso não é verdade. E é bem quando você precisar entregar o projeto atrasado, que a nova versão de um compilador pode quebrar tudo.

Esse tipo de abordagem é muito utilizado por programador de C ou programadores muito experientes acostumados com o C++ 98.

O recomendado é se manter no STL do C++ e usar as funções padrões fornecidas e testadas pelo mundo inteiro.

Mas nos casos em que é necessário usar o pragma pack, opte por usar o push e o pop.

Aprofundamento

A principal referência é o The Lost Art of Structure Packing, um guia em inglês que aborda em outras linguagens de programação.

Também é possível usar as diretivas #pragma pack(push, n) e #pragma pack(pop).

Um artigo no Geek for Geeks é sempre recomendado.

Referências