Por que o sizeof
operador retorna um tamanho maior para uma estrutura do que o tamanho total dos membros da estrutura?
Por que o sizeof
operador retorna um tamanho maior para uma estrutura do que o tamanho total dos membros da estrutura?
Respostas:
Isso ocorre devido ao preenchimento adicionado para satisfazer as restrições de alinhamento. O alinhamento da estrutura de dados afeta o desempenho e a correção dos programas:
SIGBUS
).Aqui está um exemplo usando as configurações típicas de um processador x86 (todos os modos de 32 e 64 bits usados):
struct X
{
short s; /* 2 bytes */
/* 2 padding bytes */
int i; /* 4 bytes */
char c; /* 1 byte */
/* 3 padding bytes */
};
struct Y
{
int i; /* 4 bytes */
char c; /* 1 byte */
/* 1 padding byte */
short s; /* 2 bytes */
};
struct Z
{
int i; /* 4 bytes */
short s; /* 2 bytes */
char c; /* 1 byte */
/* 1 padding byte */
};
const int sizeX = sizeof(struct X); /* = 12 */
const int sizeY = sizeof(struct Y); /* = 8 */
const int sizeZ = sizeof(struct Z); /* = 8 */
Pode-se minimizar o tamanho das estruturas, classificando os membros por alinhamento (a classificação por tamanho é suficiente para isso nos tipos básicos) (como a estrutura Z
no exemplo acima).
NOTA IMPORTANTE: Os padrões C e C ++ declaram que o alinhamento da estrutura é definido pela implementação. Portanto, cada compilador pode optar por alinhar os dados de maneira diferente, resultando em layouts de dados diferentes e incompatíveis. Por esse motivo, ao lidar com bibliotecas que serão usadas por diferentes compiladores, é importante entender como os compiladores alinham os dados. Alguns compiladores têm configurações de linha de comando e / ou #pragma
instruções especiais para alterar as configurações de alinhamento da estrutura.
Embalagem e alinhamento de bytes, conforme descrito nas Perguntas frequentes C aqui :
É para alinhamento. Muitos processadores não podem acessar quantidades de 2 e 4 bytes (por exemplo, ints e long ints) se estiverem cheios de todas as maneiras.
Suponha que você tenha esta estrutura:
struct { char a[3]; short int b; long int c; char d[3]; };
Agora, você pode pensar que deveria ser possível compactar essa estrutura na memória assim:
+-------+-------+-------+-------+ | a | b | +-------+-------+-------+-------+ | b | c | +-------+-------+-------+-------+ | c | d | +-------+-------+-------+-------+
Mas é muito, muito mais fácil no processador se o compilador organizá-lo assim:
+-------+-------+-------+ | a | +-------+-------+-------+ | b | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | +-------+-------+-------+
Na versão compactada, observe como é pelo menos um pouco difícil para você e eu ver como os campos bec estão em volta? Em poucas palavras, também é difícil para o processador. Portanto, a maioria dos compiladores preencherá a estrutura (como se tivesse campos extras e invisíveis) assim:
+-------+-------+-------+-------+ | a | pad1 | +-------+-------+-------+-------+ | b | pad2 | +-------+-------+-------+-------+ | c | +-------+-------+-------+-------+ | d | pad3 | +-------+-------+-------+-------+
s
then &s.a == &s
e &s.d == &s + 12
(dado o alinhamento mostrado na resposta). O ponteiro é armazenado apenas se as matrizes tiverem um tamanho variável (por exemplo, a
foi declarado em char a[]
vez de char a[3]
), mas os elementos deverão ser armazenados em outro lugar.
Se você deseja que a estrutura tenha um determinado tamanho no GCC, por exemplo, use __attribute__((packed))
.
No Windows, você pode definir o alinhamento como um byte ao usar o compilador cl.exe com a opção / Zp .
Geralmente é mais fácil para a CPU acessar dados múltiplos de 4 (ou 8), dependendo da plataforma e também do compilador.
Portanto, é basicamente uma questão de alinhamento.
Você precisa ter boas razões para mudar isso.
Isso pode ocorrer devido ao alinhamento e preenchimento de bytes, de modo que a estrutura tenha um número par de bytes (ou palavras) na sua plataforma. Por exemplo, em C no Linux, as três estruturas a seguir:
#include "stdio.h"
struct oneInt {
int x;
};
struct twoInts {
int x;
int y;
};
struct someBits {
int x:2;
int y:6;
};
int main (int argc, char** argv) {
printf("oneInt=%zu\n",sizeof(struct oneInt));
printf("twoInts=%zu\n",sizeof(struct twoInts));
printf("someBits=%zu\n",sizeof(struct someBits));
return 0;
}
Tenha membros com tamanhos (em bytes) de 4 bytes (32 bits), 8 bytes (2x 32 bits) e 1 byte (2 + 6 bits), respectivamente. O programa acima (no Linux usando gcc) imprime os tamanhos 4, 8 e 4 - onde a última estrutura é preenchida para que seja uma única palavra (bytes de 4 x 8 bits na minha plataforma de 32 bits).
oneInt=4
twoInts=8
someBits=4
:2
e :6
são realmente especificando 2 e 6 bits, e não cheios 32 bit inteiros neste caso. someBits.x, sendo apenas 2 bits, pode armazenar apenas 4 valores possíveis: 00, 01, 10 e 11 (1, 2, 3 e 4). Isso faz sentido? Aqui está um artigo sobre o recurso: geeksforgeeks.org/bit-fields-c
Veja também:
para Microsoft Visual C:
http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx
e GCC reivindicam compatibilidade com o compilador da Microsoft .:
http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html
Além das respostas anteriores, observe que, independentemente da embalagem, não há garantia de pedido de membros em C ++ . Os compiladores podem (e certamente acrescentam) ponteiro de tabela virtual e membros das estruturas de base à estrutura. Mesmo a existência de tabela virtual não é garantida pelo padrão (a implementação do mecanismo virtual não é especificada) e, portanto, pode-se concluir que essa garantia é simplesmente impossível.
Tenho certeza de que a ordem dos membros é garantida em C , mas não contaria com isso ao escrever um programa de plataforma cruzada ou compilador.
O tamanho de uma estrutura é maior que a soma de suas partes devido ao que é chamado de empacotamento. Um processador específico possui um tamanho de dados preferido com o qual trabalha. O tamanho preferido dos processadores mais modernos é de 32 bits (4 bytes). Acessar a memória quando os dados estão nesse tipo de limite é mais eficiente do que coisas que ultrapassam esse tamanho.
Por exemplo. Considere a estrutura simples:
struct myStruct
{
int a;
char b;
int c;
} data;
Se a máquina for uma máquina de 32 bits e os dados estiverem alinhados em um limite de 32 bits, vemos um problema imediato (assumindo que não há alinhamento da estrutura). Neste exemplo, suponhamos que os dados da estrutura iniciem no endereço 1024 (0x400 - observe que os 2 bits mais baixos são zero, portanto os dados estão alinhados com um limite de 32 bits). O acesso ao data.a funcionará bem porque inicia em um limite - 0x400. O acesso ao data.b também funcionará bem, pois está no endereço 0x404 - outro limite de 32 bits. Mas uma estrutura não alinhada colocaria data.c no endereço 0x405. Os 4 bytes de data.c estão em 0x405, 0x406, 0x407, 0x408. Em uma máquina de 32 bits, o sistema lê data.c durante um ciclo de memória, mas obtém apenas 3 dos 4 bytes (o 4º byte está no próximo limite). Portanto, o sistema teria que fazer um segundo acesso à memória para obter o 4º byte,
Agora, se em vez de colocar data.c no endereço 0x405, o compilador tiver preenchido a estrutura em 3 bytes e colocar data.c no endereço 0x408, o sistema precisará apenas de 1 ciclo para ler os dados, reduzindo o tempo de acesso a esse elemento de dados em 50%. O preenchimento troca a eficiência da memória pela eficiência do processamento. Dado que os computadores podem ter grandes quantidades de memória (muitos gigabytes), os compiladores consideram que a troca (velocidade acima do tamanho) é razoável.
Infelizmente, esse problema se torna fatal quando você tenta enviar estruturas por uma rede ou até mesmo gravar os dados binários em um arquivo binário. O preenchimento inserido entre os elementos de uma estrutura ou classe pode interromper os dados enviados para o arquivo ou rede. Para escrever código portátil (um que irá para vários compiladores diferentes), você provavelmente terá que acessar cada elemento da estrutura separadamente para garantir o "empacotamento" adequado.
Por outro lado, diferentes compiladores têm diferentes habilidades para gerenciar o empacotamento da estrutura de dados. Por exemplo, no Visual C / C ++, o compilador oferece suporte ao comando #pragma pack. Isso permitirá que você ajuste o empacotamento e o alinhamento dos dados.
Por exemplo:
#pragma pack 1
struct MyStruct
{
int a;
char b;
int c;
short d;
} myData;
I = sizeof(myData);
Agora eu deveria ter o comprimento de 11. Sem o pragma, eu poderia ter entre 11 e 14 (e para alguns sistemas, até 32), dependendo do pacote padrão do compilador.
#pragma pack
. Se os membros são alocados em seu alinhamento padrão, eu geralmente diria que a estrutura não está compactada.
Isso pode ser feito se você tiver definido implicitamente ou explicitamente o alinhamento da estrutura. Uma estrutura alinhada com 4 será sempre um múltiplo de 4 bytes, mesmo que o tamanho de seus membros seja algo que não seja múltiplo de 4 bytes.
Além disso, uma biblioteca pode ser compilada em x86 com entradas de 32 bits e você pode comparar seus componentes em um processo de 64 bits para obter um resultado diferente se você fizer isso manualmente.
Rascunho padrão C99 N1256
http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf
6.5.3.4 O tamanho do operador :
3 Quando aplicado a um operando que possui estrutura ou tipo de união, o resultado é o número total de bytes desse objeto, incluindo preenchimento interno e à direita.
6.7.2.1 Especificadores de estrutura e união :
13 ... Pode haver preenchimento sem nome dentro de um objeto de estrutura, mas não no início.
e:
15 Pode haver preenchimento sem nome no final de uma estrutura ou união.
O novo recurso do membro da matriz flexível C99 ( struct S {int is[];};
) também pode afetar o preenchimento:
16 Como um caso especial, o último elemento de uma estrutura com mais de um membro nomeado pode ter um tipo de matriz incompleto; isso é chamado de membro flexível da matriz. Na maioria das situações, o membro flexível da matriz é ignorado. Em particular, o tamanho da estrutura é como se o membro da matriz flexível fosse omitido, exceto que ele pode ter mais preenchimento à direita do que a omissão implicaria.
O Anexo J sobre questões de portabilidade reitera:
Não são especificados a seguir: ...
- O valor de bytes de preenchimento ao armazenar valores em estruturas ou uniões (6.2.6.1)
Rascunho padrão do C ++ 11 N3337
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
5.3.3 Tamanho :
2 Quando aplicado a uma classe, o resultado é o número de bytes em um objeto dessa classe, incluindo qualquer preenchimento necessário para colocar objetos desse tipo em uma matriz.
9.2 Membros da classe :
Um ponteiro para um objeto de estrutura de layout padrão, convertido adequadamente usando um reinterpret_cast, aponta para seu membro inicial (ou se esse membro é um campo de bits, depois para a unidade em que reside) e vice-versa. [Nota: Portanto, pode haver preenchimento sem nome em um objeto de estrutura de layout padrão, mas não no início, conforme necessário para obter o alinhamento apropriado. - nota final]
Eu só sei C ++ suficiente para entender a nota :-)
Além das outras respostas, uma estrutura pode (mas geralmente não possui) funções virtuais; nesse caso, o tamanho da estrutura também incluirá o espaço para o vtbl.
A linguagem C deixa ao compilador alguma liberdade sobre a localização dos elementos estruturais na memória:
A linguagem C fornece alguma garantia ao programador do layout dos elementos na estrutura:
Problemas relacionados ao alinhamento dos elementos:
Como o alinhamento funciona:
ps Informações mais detalhadas estão disponíveis aqui: "Samuel P.Harbison, Guy L.Steele CA Reference, (5.6.2 - 5.6.7)"
A idéia é que, por considerações de velocidade e cache, os operandos sejam lidos a partir de endereços alinhados ao tamanho natural. Para que isso aconteça, os membros da estrutura de almofadas do compilador serão alinhados.
struct pixel {
unsigned char red; // 0
unsigned char green; // 1
unsigned int alpha; // 4 (gotta skip to an aligned offset)
unsigned char blue; // 8 (then skip 9 10 11)
};
// next offset: 12
A arquitetura x86 sempre foi capaz de buscar endereços desalinhados. No entanto, é mais lento e quando o desalinhamento se sobrepõe a duas linhas de cache diferentes, ele despeja duas linhas de cache quando um acesso alinhado despeja apenas uma.
Algumas arquiteturas, na verdade, precisam se prender a leituras e gravações desalinhadas e a versões anteriores da arquitetura ARM (a que evoluiu para todas as CPUs móveis de hoje) ... bem, na verdade elas apenas retornaram dados ruins para elas. (Eles ignoraram os bits de baixa ordem.)
Por fim, observe que as linhas de cache podem ser arbitrariamente grandes e o compilador não tenta adivinhar ou fazer uma troca de espaço versus velocidade. Em vez disso, as decisões de alinhamento fazem parte da ABI e representam o alinhamento mínimo que acabará preenchendo uniformemente uma linha de cache.
TL; DR: o alinhamento é importante.