u/Double-Efficiency302

▲ 1 r/brdev

Visualizando como structs em C são arranjados na memória

Uma das maiores mentiras contadas ao tentar ensinar o básico de C é "Struct de C é similar a classes de Java ou Ruby".
Modelar struct é modelar bytes na memória.

Pense em C como assembly de alto nível ao invés de pensar que é uma linguagem de alto nível.

Compilando para ver o layout

Vamos compilar o código fonte C abaixo:

struct Entity {
    char active;
    double x;
    double y;
    int hp;
    char name[16];
};
​
void use(struct Entity *e);

Observe que não tem função main(). Não precisa.
Declaramos a assinatura de uma função qualquer com o uso da struct e o Clang é obrigado a identificar tamanho, offsets, alinhamento e layout/arranjo da struct na memória.

É isso que queremos. Ter uma representação visual de como aquela struct será arranjada na memória.

Vamos lá. Rode clang -Xclang -fdump-record-layouts -c entity.c -o entity no seu shell favorito.

Dumping AST Record Layout

❯ clang -Xclang -fdump-record-layouts -c entity.c -o entity
​
*** Dumping AST Record Layout
         0 | struct Entity
         0 |   char active
         8 |   double x
        16 |   double y
        24 |   int hp
        28 |   char[16] name
           | [sizeof=48, align=8]
​
*** Dumping IRgen Record Layout
Record: RecordDecl 0x753820d58 <entity.c:1:1, line:7:1> line:1:8 struct Entity definition
|-FieldDecl 0x753820e10 <line:2:3, col:8> col:8 active 'char'
|-FieldDecl 0x753820e78 <line:3:3, col:10> col:10 x 'double'
|-FieldDecl 0x753820ee0 <line:4:3, col:10> col:10 y 'double'
|-FieldDecl 0x753820f48 <line:5:3, col:7> col:7 hp 'int'
`-FieldDecl 0x753175050 <line:6:3, col:15> col:8 name 'char[16]'
​
Layout: <CGRecordLayout
  LLVMType:%struct.Entity = type { i8, double, double, i32, [16 x i8] }
  IsZeroInitializable:1
  BitFields:[
]>

Dumping AST Record Layout contém os offsets de cada membro da struct.

Membro Offset Tamanho
active 0 1 byte
x 8 a 15 8 bytes
y 16 a 23 8 bytes
hp 24 a 27 4 bytes
name 28 a 43 16 bytes

Totalizando 48 bytes. É o mesmo resultado que executar sizeof(Entity).

O layout na memória (48 bytes)

        1B         7B            8B           8B         4B           16B              4B
       ┌───┬────────────────┬────────────┬────────────┬────────┬──────────────────┬───────────┐
       │ a │ ··· padding ···│ x (double) │ y (double) │hp (int)│  name (char[16]) │··· pad ···│
       └───┴────────────────┴────────────┴────────────┴────────┴──────────────────┴───────────┘
offset:  0        1..7          8..15        16..23     24..27       28..43          44..47

Prefiro visualizar assim. Mas pera aí.

Percebeu que para o char active estão sendo usados 7 bytes a mais?

O membro char[16] name era para terminar no offset 43, totalizando 44 bytes. Mas também ganhou 4 bytes a mais que não serão usados.

Por que o compilador decidiu fazer essa sacanagem com nós escovadores de bits?

O tempo passou e eu sofri calado...

A regra de alinhamento

A regra: o processador lê memória de forma mais eficiente quando o dado está num endereço múltiplo do seu tamanho. Um double de 8 bytes nos offsets 0, 8, 16, 24... é uma leitura só. Num offset ímpar como 3, o processador pode precisar de duas leituras e juntar os pedaços (ou até gerar um fault em algumas arquiteturas).

No caso do Entity:

char active    → alignment 1
double x       → alignment 8  ← o maior
double y       → alignment 8
int hp         → alignment 4
char[16] name  → alignment 1

O maior é 8 (do double), então align=8 pra struct inteira. Isso garante que quando você tem um array de Entity, cada elemento começa num múltiplo de 8:

Entity arr[3];
// arr[0] no endereço 0    ✓ múltiplo de 8
// arr[1] no endereço 48   ✓ múltiplo de 8
// arr[2] no endereço 96   ✓ múltiplo de 8

Reordenando os membros

Observe essa nova versão do Entity:

struct Entity {
    double x;
    double y;
    char name[16];
    int hp;
    char active;
};
​
void use(struct Entity *e);

Apenas mudamos os membros de lugar e isso gerou um arranjo diferente pelo compilador.

O novo layout na memória (40 bytes)

             8B           8B              16B           4B    1B      3B
       ┌────────────┬────────────┬──────────────────┬────────┬───┬───────────┐
       │ x (double) │ y (double) │  name (char[16]) │hp (int)│ a │··· pad ···│
       └────────────┴────────────┴──────────────────┴────────┴───┴───────────┘
offset:     0..7        8..15           16..31        32..35  36    37..39

A struct agora ocupa 40 bytes totais!

Conseguir enxergar structs na memória ajuda bastante em otimizações e ter também um bom modelo mental de como o programa se comporta.

Espero que tenha sido útil.

reddit.com
u/Double-Efficiency302 — 13 days ago