Alinhamento e padding
LidoNesta aula, exploramos como o compilador organiza estruturas na memória, introduzindo os conceitos de alinhamento e padding. Você aprenderá por que o tamanho de uma struct pode ser maior que a soma de seus membros, como o compilador insere bytes de preenchimento e como reordenar campos pode otimizar o uso de memória.
Em C, a forma como as variáveis são armazenadas na memória não é tão direta quanto parece. Quando você declara uma struct, o compilador pode inserir espaços extras (padding) entre os campos para garantir que cada um esteja alinhado corretamente em relação ao endereço de memória. Esse comportamento é essencial para a eficiência do processador, mas pode surpreender quem espera que o tamanho de uma struct seja exatamente a soma dos tamanhos de seus membros.
Nesta aula, vamos desvendar os mecanismos de alinhamento e padding, entender como eles afetam o tamanho das structs e como podemos otimizar o layout para economizar memória. Você vai aprender a calcular o tamanho real de uma struct e a prever o comportamento do compilador.
Padding em structs
Padding é o nome dado aos bytes extras que o compilador insere entre os membros de uma struct (ou no final dela) para alinhar cada membro a um endereço de memória que seja múltiplo do seu tamanho. Isso é necessário porque muitos processadores acessam dados de forma mais eficiente quando eles estão alinhados — por exemplo, um inteiro de 4 bytes deve estar em um endereço divisível por 4. Se um membro não estivesse alinhado, o processador poderia precisar fazer dois acessos à memória para ler um único valor, o que prejudica o desempenho.
O compilador insere padding automaticamente, mas ele segue regras específicas: cada membro é colocado em um offset (deslocamento) que é múltiplo do seu tamanho (ou do alinhamento máximo da plataforma, que geralmente é 8 ou 16 bytes). Vamos ver um exemplo simples:
#include <stdio.h>
struct Exemplo {
char a; // 1 byte
int b; // 4 bytes
char c; // 1 byte
};
int main() {
printf("Tamanho da struct: %zu\n", sizeof(struct Exemplo));
printf("Offset de a: %zu\n", offsetof(struct Exemplo, a));
printf("Offset de b: %zu\n", offsetof(struct Exemplo, b));
printf("Offset de c: %zu\n", offsetof(struct Exemplo, c));
return 0;
}
Neste exemplo, a é um char (1 byte), b é um int (4 bytes) e c é outro char. O compilador coloca a no offset 0, b no offset 4 (não no offset 1), e c no offset 8. O padding entre a e b é de 3 bytes, e no final há mais 3 bytes para que o tamanho total da struct seja múltiplo do maior alinhamento (4 bytes). Assim, o tamanho total é 12 bytes, embora a soma dos membros seja 6 bytes.
Alinhamento
Alinhamento é a regra que determina em que endereço de memória um dado pode ser colocado. Cada tipo tem um requisito de alinhamento: por exemplo, um char tem alinhamento 1, um short tem 2, um int tem 4 (geralmente) e um double tem 8. O compilador garante que cada membro da struct esteja em um offset que seja múltiplo do seu alinhamento. Isso pode ser controlado com o atributo __attribute__((aligned(...))) em GCC ou alignas em C11, mas na maioria das vezes o comportamento padrão é suficiente.
O alinhamento máximo de uma struct é o maior alinhamento entre seus membros. Isso significa que a struct como um todo também será alinhada a esse valor. Por exemplo, se uma struct contém um double (alinhamento 8), o endereço da struct também será múltiplo de 8. O compilador insere padding no final da struct para que o tamanho total seja múltiplo do alinhamento máximo, garantindo que, em um array de structs, todas as structs estejam alinhadas corretamente.
#include <stdio.h>
#include <stddef.h>
struct Alinhado {
char a; // 1 byte
double b; // 8 bytes
};
int main() {
printf("Alinhamento da struct: %zu\n", alignof(struct Alinhado));
printf("Tamanho da struct: %zu\n", sizeof(struct Alinhado));
printf("Offset de a: %zu\n", offsetof(struct Alinhado, a));
printf("Offset de b: %zu\n", offsetof(struct Alinhado, b));
return 0;
}
Aqui, a ocupa o offset 0, e b precisa estar em um endereço múltiplo de 8, então o compilador insere 7 bytes de padding após a. O tamanho total da struct é 16 bytes (8 do double + 1 do char + 7 de padding). A struct tem alinhamento 8.
sizeof surpreendente
Muitos programadores iniciantes se surpreendem ao descobrir que sizeof de uma struct não é a soma dos tamanhos dos membros. Isso acontece justamente por causa do padding. O operador sizeof retorna o tamanho real em bytes que a struct ocupa na memória, incluindo o padding. Esse valor é importante para alocação dinâmica, cópia de structs e cálculo de arrays.
Vamos ver um exemplo clássico que mostra como a ordem dos campos afeta o tamanho:
#include <stdio.h>
struct Ordem1 {
char a;
int b;
char c;
};
struct Ordem2 {
char a;
char c;
int b;
};
int main() {
printf("sizeof(Ordem1) = %zu\n", sizeof(struct Ordem1));
printf("sizeof(Ordem2) = %zu\n", sizeof(struct Ordem2));
return 0;
}
Em muitas plataformas, sizeof(Ordem1) é 12, enquanto sizeof(Ordem2) é 8. Por quê? No primeiro caso, os campos são intercalados: char, int, char. O char inicial ocupa 1 byte, depois há 3 bytes de padding para alinhar o int, depois o int ocupa 4 bytes, depois o char final ocupa 1 byte, e mais 3 bytes de padding no final para que o tamanho seja múltiplo de 4. No segundo caso, os dois chars são colocados juntos (2 bytes), depois 2 bytes de padding para alinhar o int, e o int ocupa 4 bytes, totalizando 8 bytes sem padding final. Portanto, reordenar os campos pode reduzir o tamanho da struct.
Otimização
Otimizar o layout de structs é uma técnica para reduzir o consumo de memória, especialmente quando você tem muitas instâncias da struct (por exemplo, em arrays ou listas encadeadas). A regra geral é declarar os membros em ordem decrescente de tamanho (ou de alinhamento). Isso minimiza o padding entre os campos e reduz o tamanho total.
Vamos comparar duas versões de uma struct que armazena dados de um ponto 3D:
struct PontoRuim {
char rotulo; // 1 byte
double x; // 8 bytes
int id; // 4 bytes
double y; // 8 bytes
char cor; // 1 byte
double z; // 8 bytes
};
struct PontoBom {
double x; // 8 bytes
double y; // 8 bytes
double z; // 8 bytes
int id; // 4 bytes
char rotulo; // 1 byte
char cor; // 1 byte
};
No primeiro caso, o compilador insere muitos bytes de padding para alinhar os doubles após os chars e o int. O tamanho provavelmente será 48 bytes. No segundo caso, os doubles são colocados primeiro, depois o int e os chars juntos, resultando em um tamanho de 32 bytes. Essa diferença de 16 bytes por struct pode ser significativa em grandes coleções.
Além da ordem, você pode usar diretivas de pré-processamento ou atributos para controlar o alinhamento, como #pragma pack em alguns compiladores, que permite empacotar os campos sem padding. No entanto, isso pode prejudicar o desempenho e não é recomendado a menos que você tenha um motivo muito específico (como compatibilidade com formatos binários externos).
#include <stdio.h>
#pragma pack(push, 1) // Empacota sem padding
struct Empacotado {
char a;
int b;
char c;
};
#pragma pack(pop)
int main() {
printf("sizeof(Empacotado) = %zu\n", sizeof(struct Empacotado));
return 0;
}
Neste exemplo, o tamanho é 6 bytes (1+4+1), mas o acesso a b pode ser mais lento. Use com cuidado.
Boas práticas
Aqui estão algumas recomendações para lidar com alinhamento e padding:
- Declare os membros em ordem decrescente de tamanho (ou alinhamento) para minimizar o padding.
- Use
offsetofpara verificar os offsets em tempo de execução, se necessário. - Evite
#pragma packa menos que seja absolutamente necessário; ele pode causar problemas de desempenho e portabilidade. - Lembre-se de que o tamanho de uma struct pode variar entre compiladores e plataformas; não assuma valores fixos.
- Para serialização ou comunicação com outros sistemas, defina explicitamente o layout e use tipos de tamanho fixo (como
int32_t).
Referências
- cppreference - Struct and union initialization
- LearnCPP - Structs (embora seja C++, os conceitos de layout são semelhantes)
- GeeksforGeeks - Structure Member Alignment, Padding and Data Packing
- Stack Overflow - Why isn't sizeof for a struct equal to the sum of sizeof of each member?
- IBM Documentation - Alignment and padding
- GCC - Structure Layout Pragmas
Exercícios
- Declare uma struct chamada
Pessoacom campos:nome(char[20]),idade(int),altura(double) esexo(char). Usesizeofeoffsetofpara imprimir o tamanho e os offsets de cada campo. Qual é o tamanho da struct?✓ Resposta: O tamanho depende da plataforma, mas em um sistema de 64 bits com alinhamento padrão, a struct terá 40 bytes. O array de char[20] ocupa 20 bytes, depois 4 bytes de padding para alinhar o int, o int ocupa 4 bytes, depois 4 bytes de padding para alinhar o double, o double ocupa 8 bytes, e o char final ocupa 1 byte, com 7 bytes de padding no final para alinhamento máximo (8). Os offsets são: nome=0, idade=20, altura=28, sexo=36. O tamanho total é 40. Código de exemplo:#include <stdio.h> #include <stddef.h> struct Pessoa { char nome[20]; int idade; double altura; char sexo; }; int main() { printf("Tamanho: %zu\n", sizeof(struct Pessoa)); printf("Offset nome: %zu\n", offsetof(struct Pessoa, nome)); printf("Offset idade: %zu\n", offsetof(struct Pessoa, idade)); printf("Offset altura: %zu\n", offsetof(struct Pessoa, altura)); printf("Offset sexo: %zu\n", offsetof(struct Pessoa, sexo)); return 0; } - Considere a struct
struct A { char c; double d; };Qual é o tamanho da struct e qual é o offset ded? Explique o motivo.✓ Resposta: O tamanho é 16 bytes e o offset dedé 8. Isso ocorre porque ocharocupa 1 byte, mas odoubleprecisa estar alinhado a 8 bytes, então o compilador insere 7 bytes de padding após o char. O tamanho total é 16 (8 do double + 1 do char + 7 de padding). Código de verificação:#include <stdio.h> #include <stddef.h> struct A { char c; double d; }; int main() { printf("sizeof(struct A) = %zu\n", sizeof(struct A)); printf("offsetof(d) = %zu\n", offsetof(struct A, d)); return 0; } - Reordene os campos da struct do exercício 1 para minimizar o tamanho. Qual é o novo tamanho? Mostre a nova struct.✓ Resposta: A ordem otimizada é: altura (double), nome (char[20]), idade (int), sexo (char). O tamanho passa a ser 32 bytes. Os offsets são: altura=0, nome=8, idade=28, sexo=32. Código:
O double ocupa 8 bytes, o char[20] ocupa 20 bytes (offset 8), o int ocupa 4 bytes (offset 28), e o char ocupa 1 byte (offset 32). O tamanho total é 32, pois o alinhamento máximo é 8 e 32 é múltiplo de 8.struct PessoaOtimizada { double altura; char nome[20]; int idade; char sexo; }; - Escreva um programa que use
#pragma pack(1)para empacotar a struct do exercício 1 e imprima o tamanho. Qual é a diferença? Discuta as implicações de desempenho.✓ Resposta: Com#pragma pack(1), o tamanho da struct original (com nome[20], idade, altura, sexo) seria 20 + 4 + 8 + 1 = 33 bytes. A diferença é de 7 bytes a menos. Porém, o acesso aalturaeidadepode ser mais lento, pois eles não estarão alinhados naturalmente. Exemplo:#include <stdio.h> #pragma pack(push, 1) struct PessoaPacked { char nome[20]; int idade; double altura; char sexo; }; #pragma pack(pop) int main() { printf("sizeof(PessoaPacked) = %zu\n", sizeof(struct PessoaPacked)); return 0; } - Usando
offsetof, crie um programa que imprima os offsets de cada campo da structstruct Exemplo { char a; int b; char c; };e verifique se o padding é visível. Explique como você pode usar essas informações para otimizar o layout manualmente.✓ Resposta: O programa é:
Os offsets serão 0, 4 e 8, e o tamanho 12. O padding está entre a e b (3 bytes) e no final (3 bytes). Para otimizar, reordene para#include <stdio.h> #include <stddef.h> struct Exemplo { char a; int b; char c; }; int main() { printf("Offset de a: %zu\n", offsetof(struct Exemplo, a)); printf("Offset de b: %zu\n", offsetof(struct Exemplo, b)); printf("Offset de c: %zu\n", offsetof(struct Exemplo, c)); printf("Tamanho: %zu\n", sizeof(struct Exemplo)); return 0; }char a; char c; int b;, que terá tamanho 8. Você pode usar essas informações para decidir a ordem dos campos em suas próprias structs.