Por que sizeof para uma estrutura é igual à soma do sizeof de cada membro?


697

Por que o sizeofoperador retorna um tamanho maior para uma estrutura do que o tamanho total dos membros da estrutura?


14
Consulte esta C FAQ sobre o alighnment de memória. c-faq.com/struct/align.esr.html
Richard Chambers

48
Anedota: Havia um vírus de computador real que colocava seu código dentro dos preenchimentos de estrutura no programa host.
El13 Elazar

4
@Elazar Isso é impressionante! Eu nunca teria pensado ser possível usar áreas tão pequenas para qualquer coisa. Você é capaz de fornecer mais detalhes?
Wilson

1
@ Wilson - Tenho certeza de que envolveu muito jmp.
hoodaticus

4
Ver estrutura de acolchoamento, embalagem : A arte perdida de estrutura C de embalagem Eric S. Raymond
EsmaeelE

Respostas:


649

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:

  • O acesso desalinhado pode ser um erro grave (geralmente SIGBUS).
  • Acesso desalinhado pode ser um erro leve.
    • Corrigido no hardware, para uma degradação de desempenho modesta.
    • Ou corrigido pela emulação no software, para uma grave degradação do desempenho.
    • Além disso, a atomicidade e outras garantias de simultaneidade podem ser quebradas, levando a erros sutis.

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 Zno 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 #pragmainstruções especiais para alterar as configurações de alinhamento da estrutura.


38
Quero anotar aqui: a maioria dos processadores penaliza você por acesso desalinhado à memória (como você mencionou), mas você não pode esquecer que muitos o desaprovam completamente. A maioria dos chips MIPS, em particular, lançará uma exceção em um acesso não alinhado.
Cody Brocious 23/09/08

35
Os chips x86 são realmente únicos, pois permitem acesso desalinhado, embora penalizados; A maioria dos chips AFAIK lançará exceções, não apenas algumas. PowerPC é outro exemplo comum.
Dark Shikari

6
A ativação de pragmas para acessos desalinhados geralmente faz com que seu código seja inflado em tamanho, em processadores que geram falhas de desalinhamento, pois é necessário gerar código para corrigir todos os desalinhamentos. O ARM também lança falhas de desalinhamento.
Mike Dimmick

5
@ Dark - concordo totalmente. Mas a maioria dos processadores para desktop são x86 / x64, por isso a maioria dos chips não emitir falhas de alinhamento de dados;)
Aaron

27
O acesso não alinhado aos dados é geralmente um recurso encontrado nas arquiteturas CISC, e a maioria das arquiteturas RISC não o inclui (ARM, MIPS, PowerPC, Cell). Na verdade, a maioria dos chips NÃO são processadores de desktop, para regra incorporada por número de chips e a grande maioria deles são arquiteturas RISC.
Lara Dougan

191

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  |
+-------+-------+-------+-------+

1
Agora, qual é o uso dos slots de memória pad1, pad2 e pad3.
Lakshmi Sreekanth Chitla


@EmmEff isso pode estar errado, mas eu não entendo direito: por que não há slot de memória para o ponteiro nas matrizes?
Balázs Börcsök 07/12/19

1
@ BalázsBörcsök São matrizes de tamanho constante e, portanto, seus elementos são armazenados diretamente na estrutura com compensações fixas. O compilador sabe tudo isso em tempo de compilação, portanto o ponteiro está implícito. Por exemplo, se você tiver uma variável struct desse tipo chamada sthen &s.a == &se &s.d == &s + 12(dado o alinhamento mostrado na resposta). O ponteiro é armazenado apenas se as matrizes tiverem um tamanho variável (por exemplo, afoi declarado em char a[]vez de char a[3]), mas os elementos deverão ser armazenados em outro lugar.
kbolino 31/03

27

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.


5
"boas razões" Exemplo: Mantendo a compatibilidade binária (preenchimento) consistente entre sistemas de 32 e 64 bits para uma estrutura complexa no código de demonstração de prova de conceito que será exibido amanhã. Às vezes, a necessidade tem precedência sobre a propriedade.
Mr.Ree 8/08/08

2
Está tudo bem, exceto quando você menciona o sistema operacional. Esse é um problema para a velocidade da CPU, o sistema operacional não está envolvido.
Blaisorblade 12/01/09

3
Outra boa razão é se você estiver inserindo um fluxo de dados em uma estrutura, por exemplo, ao analisar protocolos de rede.
ceo

1
@dolmen Acabei de salientar que "é mais fácil para o Sistema Operatin acessar dados" está incorreto, pois o sistema operacional não acessa dados.
Blaisorblade

1
@dolmen De fato, deve-se falar sobre a ABI (interface binária do aplicativo). O alinhamento padrão (usado se você não o alterar na fonte) depende da ABI, e muitos SOs suportam várias ABIs (por exemplo, 32 e 64 bits, ou para binários de diferentes SOs, ou para diferentes maneiras de compilar o mesmos binários para o mesmo sistema operacional). OTOH, qual alinhamento é conveniente em termos de desempenho depende da CPU - a memória é acessada da mesma maneira, se você usa o modo de 32 ou 64 bits (não posso comentar no modo real, mas parece pouco relevante para o desempenho atualmente). O IIRC Pentium começou a preferir o alinhamento de 8 bytes.
Blaisorblade

15

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

4
"C no Linux usando gcc" não é suficiente para descrever sua plataforma. O alinhamento depende principalmente da arquitetura da CPU.
dólmen

- @ Kyle Burton. Com licença, não entendo por que o tamanho da estrutura "someBits" é igual a 4, espero 8 bytes, pois existem 2 números inteiros declarados (2 * sizeof (int)) = 8 bytes. graças
youpilat13

1
Oi @ youpilat13, o :2e :6sã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
Kyle Burton

11

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.


4
"Tenho certeza de que a ordem dos membros é grunhida em C". Sim, C99 diz: "Dentro de um objeto de estrutura, os membros que não são de campo de bits e as unidades nas quais os campos de bits residem têm endereços que aumentam na ordem em que são declarados." Mais bondade padrão em: stackoverflow.com/a/37032302/895245
Ciro Santilli 郝海东 冠状 病 六四 事件


8

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.


Isso discute as consequências do preenchimento da estrutura, mas não responde à pergunta.
Keith Thompson

" ... por causa do que é chamado de empacotamento. ... - acho que você quer dizer" preenchimento "." Tamanho preferido dos processadores mais modernos se 32 bits (4 bytes) "- Isso é um pouco simplista. Normalmente tamanhos de 8, 16, 32 e 64 bits são suportados, muitas vezes cada tamanho tem o seu próprio alinhamento e eu não tenho certeza sua resposta acrescenta qualquer nova informação que não é já na resposta aceita..
Keith Thompson

1
Quando eu disse empacotando, quis dizer como o compilador empacota os dados em uma estrutura (e pode fazê-lo preenchendo os itens pequenos, mas não precisa preencher, mas sempre empacota). Quanto ao tamanho - eu estava falando sobre a arquitetura do sistema, não sobre o que o sistema oferecerá suporte para acesso a dados (que é muito diferente da arquitetura de barramento subjacente). Quanto ao seu comentário final, dei uma explicação simplificada e ampliada de um aspecto da troca (velocidade versus tamanho) - um grande problema de programação. Também descrevo uma maneira de corrigir o problema - que não estava na resposta aceita.
sid1138

"Empacotar" neste contexto geralmente se refere à alocação de membros mais rigorosamente do que o padrão, como acontece com #pragma pack. Se os membros são alocados em seu alinhamento padrão, eu geralmente diria que a estrutura não está compactada.
Keith Thompson

Embalagem é uma espécie de termo sobrecarregado. Significa como você coloca os elementos da estrutura na memória. Semelhante ao significado de colocar objetos em uma caixa (embalagem para movimentação). Isso também significa colocar elementos na memória sem preenchimento (uma espécie de mão curta para "bem compactado"). Depois, há a versão de comando da palavra no comando #pragma pack.
sid1138

5

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.


5

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 :-)


4

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.


8
Não é bem assim. Em implementações típicas, o que é adicionado à estrutura é um ponteiro de tabela .
Don Wakefield

3

A linguagem C deixa ao compilador alguma liberdade sobre a localização dos elementos estruturais na memória:

  • furos de memória podem aparecer entre dois componentes e após o último componente. Isso se deve ao fato de que certos tipos de objetos no computador de destino podem ser limitados pelos limites de endereçamento.
  • tamanho de "furos de memória" incluído no resultado do operador sizeof. O tamanho somente não inclui o tamanho da matriz flexível, disponível em C / C ++
  • Algumas implementações da linguagem permitem controlar o layout da memória das estruturas através das opções pragma e compilador

A linguagem C fornece alguma garantia ao programador do layout dos elementos na estrutura:

  • compiladores necessários para atribuir uma sequência de componentes aumentando os endereços de memória
  • O endereço do primeiro componente coincide com o endereço inicial da estrutura
  • campos de bits sem nome podem ser incluídos na estrutura para os alinhamentos de endereço necessários dos elementos adjacentes

Problemas relacionados ao alinhamento dos elementos:

  • Computadores diferentes alinham as bordas dos objetos de maneiras diferentes
  • Restrições diferentes na largura do campo de bits
  • Os computadores diferem em como armazenar os bytes em uma palavra (Intel 80x86 e Motorola 68000)

Como o alinhamento funciona:

  • O volume ocupado pela estrutura é calculado como o tamanho do elemento único alinhado de uma matriz dessas estruturas. A estrutura deve terminar para que o primeiro elemento da próxima estrutura a seguir não viole os requisitos de alinhamento

ps Informações mais detalhadas estão disponíveis aqui: "Samuel P.Harbison, Guy L.Steele CA Reference, (5.6.2 - 5.6.7)"


2

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.