Pourquoi sizeof pour une structure n'est-il pas égal à la somme de sizeof de chaque membre?

697

Pourquoi l' sizeofopérateur renvoie-t-il une taille plus grande pour une structure que la taille totale des membres de la structure?

Kevin
la source
14
Voir cette FAQ C sur l’amélioration de la mémoire. c-faq.com/struct/align.esr.html
Richard Chambers
48
Anecdote: Il y avait un virus informatique réel qui a mis son code dans des rembourrages struct dans le programme hôte.
Elazar
4
@Elazar C'est impressionnant! Je n'aurais jamais pensé qu'il était possible d'utiliser de si petites surfaces pour quoi que ce soit. Pouvez-vous fournir plus de détails?
Wilson
1
@Wilson - Je suis sûr que cela impliquait beaucoup de jmp.
hoodaticus
4
Voir le rembourrage de la structure , l'emballage : The Lost Art of C Structure Packing Eric S. Raymond
EsmaeelE

Réponses:

649

Cela est dû au remplissage ajouté pour satisfaire les contraintes d'alignement. L'alignement de la structure des données a un impact sur les performances et l'exactitude des programmes:

  • Un accès mal aligné peut être une erreur grave (souvent SIGBUS).
  • Un accès mal aligné peut être une erreur logicielle.
    • Soit corrigé dans le matériel, pour une dégradation modeste des performances.
    • Ou corrigé par émulation dans le logiciel, pour une dégradation sévère des performances.
    • De plus, l'atomicité et d'autres garanties de concurrence peuvent être rompues, ce qui entraîne des erreurs subtiles.

Voici un exemple utilisant des paramètres typiques pour un processeur x86 (tous les modes 32 et 64 bits utilisés):

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 */

On peut minimiser la taille des structures en triant les membres par alignement (le tri par taille suffit pour cela dans les types basiques) (comme la structure Zdans l'exemple ci-dessus).

REMARQUE IMPORTANTE: les normes C et C ++ indiquent que l'alignement de la structure est défini par l'implémentation. Par conséquent, chaque compilateur peut choisir d'aligner les données différemment, ce qui entraîne des dispositions de données différentes et incompatibles. Pour cette raison, lorsqu'il s'agit de bibliothèques qui seront utilisées par différents compilateurs, il est important de comprendre comment les compilateurs alignent les données. Certains compilateurs ont des paramètres de ligne de commande et / ou des #pragmainstructions spéciales pour modifier les paramètres d'alignement de la structure.

Kevin
la source
38
Je veux faire une note ici: la plupart des processeurs vous pénalisent pour l'accès à la mémoire non aligné (comme vous l'avez mentionné), mais vous ne pouvez pas oublier que beaucoup le refusent complètement. La plupart des puces MIPS, en particulier, lèveront une exception sur un accès non aligné.
Cody Brocious
35
Les puces x86 sont en fait plutôt uniques en ce sens qu'elles permettent un accès non aligné, bien que pénalisé; AFAIK la plupart des puces lèveront des exceptions, pas seulement quelques-unes. PowerPC est un autre exemple courant.
Dark Shikari
6
L'activation des pragmas pour les accès non alignés entraîne généralement une augmentation de la taille de votre code, sur les processeurs qui génèrent des défauts de désalignement, car un code pour corriger chaque désalignement doit être généré. ARM génère également des défauts de désalignement.
Mike Dimmick
5
@Dark - tout à fait d'accord. Mais la plupart des processeurs de bureau sont x86 / x64, donc la plupart des puces n'émettent pas de défauts d'alignement des données;)
Aaron
27
L'accès aux données non alignées est généralement une caractéristique des architectures CISC, et la plupart des architectures RISC ne l'incluent pas (ARM, MIPS, PowerPC, Cell). En fait, la plupart des puces ne sont PAS des processeurs de bureau, pour une règle intégrée par nombre de puces et la grande majorité d'entre elles sont des architectures RISC.
Lara Dougan
191

Emballage et alignement des octets, comme décrit dans la FAQ C ici :

C'est pour l'alignement. De nombreux processeurs ne peuvent pas accéder à des quantités de 2 et 4 octets (par exemple, les entiers et les entiers longs) s'ils sont entassés dans tous les sens.

Supposons que vous ayez cette structure:

struct {
    char a[3];
    short int b;
    long int c;
    char d[3];
};

Maintenant, vous pourriez penser qu'il devrait être possible de compresser cette structure en mémoire comme ceci:

+-------+-------+-------+-------+
|           a           |   b   |
+-------+-------+-------+-------+
|   b   |           c           |
+-------+-------+-------+-------+
|   c   |           d           |
+-------+-------+-------+-------+

Mais c'est beaucoup, beaucoup plus facile sur le processeur si le compilateur l'arrange comme ceci:

+-------+-------+-------+
|           a           |
+-------+-------+-------+
|       b       |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           |
+-------+-------+-------+

Dans la version compactée, remarquez comment il est au moins un peu difficile pour vous et moi de voir comment les champs b et c s'enroulent? En un mot, c'est difficile pour le processeur aussi. Par conséquent, la plupart des compilateurs remplissent la structure (comme avec des champs supplémentaires et invisibles) comme ceci:

+-------+-------+-------+-------+
|           a           | pad1  |
+-------+-------+-------+-------+
|       b       |     pad2      |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           | pad3  |
+-------+-------+-------+-------+
EmmEff
la source
1
Maintenant, à quoi servent les emplacements mémoire pad1, pad2 et pad3.
Lakshmi Sreekanth Chitla
7
@YoYoYonnY ce n'est pas possible. Le compilateur n'est pas autorisé à réorganiser les membres de la structure bien que gcc ait une option expérimentale pour le faire
phuclv
@EmmEff cela peut être faux mais je ne le comprends pas: pourquoi n'y a-t-il pas de slot mémoire pour le pointeur dans les tableaux?
Balázs Börcsök
1
@ BalázsBörcsök Ce sont des tableaux de taille constante, et donc leurs éléments sont stockés directement dans la structure à des décalages fixes. Le compilateur sait tout cela au moment de la compilation, donc le pointeur est implicite. Par exemple, si vous avez une variable struct de ce type appelée salors &s.a == &set &s.d == &s + 12(étant donné l'alignement indiqué dans la réponse). Le pointeur n'est stocké que si les tableaux ont une taille variable (par exemple, a aété déclaré à la char a[]place de char a[3]), mais les éléments doivent alors être stockés ailleurs.
kbolino
27

Si vous voulez que la structure ait une certaine taille avec GCC par exemple, utilisez __attribute__((packed)).

Sous Windows, vous pouvez définir l'alignement sur un octet lorsque vous utilisez le compilateur cl.exe avec l' option / Zp .

Habituellement, il est plus facile pour le CPU d'accéder à des données qui sont un multiple de 4 (ou 8), selon la plate-forme et également sur le compilateur.

C'est donc essentiellement une question d'alignement.

Vous devez avoir de bonnes raisons de le changer.

INS
la source
5
"bonnes raisons" Exemple: Garder la compatibilité binaire (remplissage) cohérente entre les systèmes 32 bits et 64 bits pour une structure complexe dans le code de démonstration de preuve de concept qui sera présenté demain. Parfois, la nécessité doit prévaloir sur la convenance.
Mr.Ree
2
Tout va bien sauf lorsque vous mentionnez le système d'exploitation. C'est un problème pour la vitesse du CPU, le système d'exploitation n'est pas impliqué du tout.
Blaisorblade
3
Une autre bonne raison est que si vous insérez un flux de données dans une structure, par exemple lors de l'analyse de protocoles réseau.
PDG
1
@dolmen Je viens de souligner qu '"il est plus facile pour le système Operatin d'accéder aux données" est incorrect, car le système d'exploitation n'accède pas aux données.
Blaisorblade
1
@dolmen En fait, il faut parler de l'ABI (application binary interface). L'alignement par défaut (utilisé si vous ne le changez pas dans la source) dépend de l'ABI, et de nombreux systèmes d'exploitation prennent en charge plusieurs ABI (par exemple, 32 et 64 bits, ou pour les binaires de différents systèmes d'exploitation, ou pour différentes manières de compiler le mêmes binaires pour le même OS). OTOH, quel alignement est pratique en termes de performances dépend du CPU - la mémoire est accessible de la même manière que vous utilisiez le mode 32 ou 64 bits (je ne peux pas commenter le mode réel, mais semble peu pertinent pour les performances de nos jours). IIRC Pentium a commencé à préférer l'alignement sur 8 octets.
Blaisorblade
15

Cela peut être dû à l'alignement des octets et au remplissage afin que la structure émette un nombre pair d'octets (ou de mots) sur votre plate-forme. Par exemple en C sous Linux, les 3 structures suivantes:

#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;
}

Les membres dont la taille (en octets) sont respectivement de 4 octets (32 bits), 8 octets (2x 32 bits) et 1 octet (2 + 6 bits). Le programme ci-dessus (sous Linux utilisant gcc) imprime les tailles 4, 8 et 4 - où la dernière structure est complétée de manière à ce qu'il s'agisse d'un seul mot (4 x 8 octets sur ma plate-forme 32 bits).

oneInt=4
twoInts=8
someBits=4
Kyle Burton
la source
4
"C sur Linux utilisant gcc" ne suffit pas pour décrire votre plateforme. L'alignement dépend principalement de l'architecture du processeur.
dolmen
- @ Kyle Burton. Excusez-moi, je ne comprends pas pourquoi la taille de la structure "someBits" est égale à 4, j'attends 8 octets car il y a 2 entiers déclarés (2 * sizeof (int)) = 8 octets. merci
youpilat13
1
Salut @ youpilat13, le :2et :6spécifient en fait des entiers 2 et 6 bits, et non 32 bits dans ce cas. someBits.x, étant seulement 2 bits, ne peut stocker que 4 valeurs possibles: 00, 01, 10 et 11 (1, 2, 3 et 4). Est-ce que ça a du sens? Voici un article sur la fonctionnalité: geeksforgeeks.org/bit-fields-c
Kyle Burton
11

Voir également:

pour Microsoft Visual C:

http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=vs.80%29.aspx

et GCC revendiquent la compatibilité avec le compilateur de Microsoft .:

http://gcc.gnu.org/onlinedocs/gcc/Structure_002dPacking-Pragmas.html

En plus des réponses précédentes, veuillez noter que quel que soit l'emballage, il n'y a pas de garantie de commande des membres en C ++ . Les compilateurs peuvent (et certainement le font) ajouter un pointeur de table virtuelle et des membres de structures de base à la structure. Même l'existence d'une table virtuelle n'est pas assurée par la norme (l'implémentation du mécanisme virtuel n'est pas spécifiée) et on peut donc conclure qu'une telle garantie est tout simplement impossible.

Je suis sûr que l' ordre des membres est garanti en C , mais je ne compterais pas dessus lors de l'écriture d'un programme multiplateforme ou compilateur croisé.

lkanab
la source
4
"Je suis sûr que l'ordre des membres est grogné en C". Oui, C99 dit: "Dans un objet de structure, les membres non-champ de bits et les unités dans lesquelles résident les champs de bits ont des adresses qui augmentent dans l'ordre dans lequel ils sont déclarés." Plus de bonté standard à: stackoverflow.com/a/37032302/895245
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
8

La taille d'une structure est supérieure à la somme de ses parties en raison de ce qu'on appelle l'emballage. Un processeur particulier a une taille de données préférée avec laquelle il fonctionne. Taille préférée des processeurs les plus modernes si 32 bits (4 octets). Accéder à la mémoire lorsque des données se trouvent sur ce type de limite est plus efficace que les éléments qui chevauchent cette limite de taille.

Par exemple. Considérez la structure simple:

struct myStruct
{
   int a;
   char b;
   int c;
} data;

Si la machine est une machine 32 bits et que les données sont alignées sur une frontière 32 bits, nous voyons un problème immédiat (en supposant qu'il n'y ait pas d'alignement de structure). Dans cet exemple, supposons que les données de structure commencent à l'adresse 1024 (0x400 - notez que les 2 bits les plus bas sont nuls, donc les données sont alignées sur une limite de 32 bits). L'accès à data.a fonctionnera bien car il commence sur une limite - 0x400. L'accès à data.b fonctionnera également très bien, car il se trouve à l'adresse 0x404 - une autre limite de 32 bits. Mais une structure non alignée mettrait data.c à l'adresse 0x405. Les 4 octets de data.c sont à 0x405, 0x406, 0x407, 0x408. Sur une machine 32 bits, le système lirait data.c pendant un cycle de mémoire, mais n'obtiendrait que 3 des 4 octets (le 4e octet se trouve sur la limite suivante). Donc, le système devrait faire un deuxième accès à la mémoire pour obtenir le 4e octet,

Maintenant, si au lieu de mettre data.c à l'adresse 0x405, le compilateur a complété la structure de 3 octets et mis data.c à l'adresse 0x408, alors le système n'aurait besoin que d'un cycle pour lire les données, réduisant le temps d'accès à cet élément de données de 50%. Le rembourrage permute l'efficacité de la mémoire pour l'efficacité du traitement. Étant donné que les ordinateurs peuvent avoir d'énormes quantités de mémoire (plusieurs gigaoctets), les compilateurs estiment que l'échange (vitesse par rapport à la taille) est raisonnable.

Malheureusement, ce problème devient un tueur lorsque vous essayez d'envoyer des structures sur un réseau ou même d'écrire les données binaires dans un fichier binaire. Le remplissage inséré entre les éléments d'une structure ou d'une classe peut perturber les données envoyées au fichier ou au réseau. Afin d'écrire du code portable (celui qui ira à plusieurs compilateurs différents), vous devrez probablement accéder à chaque élément de la structure séparément pour assurer le bon "emballage".

D'un autre côté, différents compilateurs ont des capacités différentes pour gérer l'empaquetage de la structure de données. Par exemple, dans Visual C / C ++, le compilateur prend en charge la commande #pragma pack. Cela vous permettra d'ajuster la compression et l'alignement des données.

Par exemple:

#pragma pack 1
struct MyStruct
{
    int a;
    char b;
    int c;
    short d;
} myData;

I = sizeof(myData);

Je devrais maintenant avoir la longueur de 11. Sans le pragma, je pourrais être n'importe quoi de 11 à 14 (et pour certains systèmes, jusqu'à 32), en fonction de l'emballage par défaut du compilateur.

sid1138
la source
Ceci discute des conséquences du remplissage de la structure, mais cela ne répond pas à la question.
Keith Thompson
" ... à cause de ce qu'on appelle l'emballage. ... - Je pense que vous voulez dire" remplissage "." La taille préférée des processeurs modernes si 32 bits (4 octets) "- C'est un peu une simplification excessive. Typiquement les tailles de 8, 16, 32 et 64 bits sont prises en charge; souvent chaque taille a son propre alignement. Et je ne suis pas sûr que votre réponse ajoute de nouvelles informations qui ne sont pas déjà dans la réponse acceptée.
Keith Thompson
1
Quand j'ai dit emballer, je voulais dire comment le compilateur compresse les données dans une structure (et il peut le faire en remplissant les petits éléments, mais il n'a pas besoin de remplir, mais il compresse toujours). Quant à la taille - je parlais de l'architecture du système, pas de ce que le système prendra en charge pour l'accès aux données (ce qui est bien différent de l'architecture de bus sous-jacente). Quant à votre dernier commentaire, j'ai donné une explication simplifiée et élargie d'un aspect du compromis (vitesse contre taille) - un problème de programmation majeur. Je décris également un moyen de résoudre le problème - ce n'était pas dans la réponse acceptée.
sid1138
"Emballage" dans ce contexte se réfère généralement à l'allocation des membres plus étroitement que par défaut, comme avec #pragma pack. Si les membres sont alloués sur leur alignement par défaut, je dirais généralement que la structure n'est pas compactée.
Keith Thompson
L'emballage est une sorte de terme surchargé. Cela signifie comment vous mettez des éléments de structure en mémoire. Similaire à la signification de mettre des objets dans une boîte (emballage pour le déplacement). Cela signifie également mettre des éléments en mémoire sans remplissage (une sorte de main courte pour "bien emballé"). Ensuite, il y a la version de commande du mot dans la commande #pragma pack.
sid1138
5

Il peut le faire si vous avez implicitement ou explicitement défini l'alignement de la structure. Une structure alignée sur 4 sera toujours un multiple de 4 octets même si la taille de ses membres serait quelque chose qui n'est pas un multiple de 4 octets.

Une bibliothèque peut également être compilée sous x86 avec des entiers 32 bits et vous pourriez comparer ses composants sur un processus 64 bits vous donnerait un résultat différent si vous le faisiez à la main.

Orion Adrian
la source
5

Projet standard C99 N1256

http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

6.5.3.4 L'opérateur sizeof :

3 Lorsqu'il est appliqué à un opérande qui a une structure ou un type d'union, le résultat est le nombre total d'octets dans un tel objet, y compris le remplissage interne et final.

6.7.2.1 Spécificateurs de structure et d'union :

13 ... Il peut y avoir un remplissage sans nom dans un objet de structure, mais pas à son début.

et:

15 Il peut y avoir un rembourrage sans nom à la fin d'une structure ou d'union.

La nouvelle fonction de membre de tableau flexible C99 ( struct S {int is[];};) peut également affecter le remplissage:

16 Comme cas particulier, le dernier élément d'une structure avec plus d'un membre nommé peut avoir un type de tableau incomplet; c'est ce qu'on appelle un membre de tableau flexible. Dans la plupart des situations, le membre du tableau flexible est ignoré. En particulier, la taille de la structure est comme si le membre de tableau flexible était omis, sauf qu'il peut avoir plus de remplissage de fin que l'omission impliquerait.

L'annexe J, Questions de portabilité, réitère:

Les éléments suivants ne sont pas spécifiés: ...

  • La valeur des octets de remplissage lors du stockage de valeurs dans des structures ou des unions (6.2.6.1)

Projet standard C ++ 11 N3337

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

5.3.3 Taille de :

2 Lorsqu'il est appliqué à une classe, le résultat est le nombre d'octets dans un objet de cette classe, y compris tout remplissage requis pour placer des objets de ce type dans un tableau.

9.2 Membres du groupe :

Un pointeur vers un objet struct de mise en page standard, convenablement converti à l'aide d'un reinterpret_cast, pointe vers son membre initial (ou si ce membre est un champ binaire, puis vers l'unité dans laquelle il réside) et vice versa. [Remarque: Il peut donc y avoir un remplissage sans nom dans un objet struct de mise en page standard, mais pas au début, comme nécessaire pour obtenir un alignement approprié. - note de fin]

Je ne connais que suffisamment de C ++ pour comprendre la note :-)

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
4

En plus des autres réponses, une structure peut (mais généralement pas) avoir des fonctions virtuelles, auquel cas la taille de la structure inclura également l'espace pour le vtbl.

JohnMcG
la source
8
Pas assez. Dans les implémentations typiques, ce qui est ajouté à la structure est un pointeur vtable .
Don Wakefield
3

Le langage C laisse au compilateur une certaine liberté quant à l'emplacement des éléments structurels dans la mémoire:

  • des trous de mémoire peuvent apparaître entre deux composants quelconques et après le dernier composant. Cela est dû au fait que certains types d'objets sur l'ordinateur cible peuvent être limités par les limites de l'adressage
  • "trous de mémoire" taille incluse dans le résultat de la taille de l'opérateur. La taille de seulement n'inclut pas la taille du tableau flexible, qui est disponible en C / C ++
  • Certaines implémentations du langage vous permettent de contrôler la disposition de la mémoire des structures via le pragma et les options du compilateur

Le langage C fournit une certaine assurance au programmeur de la disposition des éléments dans la structure:

  • compilateurs requis pour affecter une séquence de composants augmentant les adresses mémoire
  • L'adresse du premier composant coïncide avec l'adresse de début de la structure
  • des champs de bits sans nom peuvent être inclus dans la structure aux alignements d'adresse requis des éléments adjacents

Problèmes liés à l'alignement des éléments:

  • Différents ordinateurs alignent les bords des objets de différentes manières
  • Différentes restrictions sur la largeur du champ de bits
  • Les ordinateurs diffèrent sur la façon de stocker les octets dans un mot (Intel 80x86 et Motorola 68000)

Fonctionnement de l'alignement:

  • Le volume occupé par la structure est calculé comme la taille de l'élément unique aligné d'un réseau de telles structures. La structure doit se terminer de sorte que le premier élément de la structure suivante ne respecte pas les exigences d'alignement

ps Des informations plus détaillées sont disponibles ici: "Samuel P.Harbison, Guy L.Steele CA Reference, (5.6.2 - 5.6.7)"

bruziuz
la source
2

L'idée est que pour des considérations de vitesse et de cache, les opérandes doivent être lus à partir d'adresses alignées à leur taille naturelle. Pour ce faire, les compilateurs compilent les membres de sorte que le membre suivant ou la structure suivante soit aligné.

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

L'architecture x86 a toujours pu récupérer des adresses mal alignées. Cependant, il est plus lent et lorsque le désalignement chevauche deux lignes de cache différentes, il expulse deux lignes de cache lorsqu'un accès aligné n'en expulse qu'une.

Certaines architectures doivent en fait piéger les lectures et les écritures mal alignées, et les premières versions de l'architecture ARM (celle qui a évolué dans tous les CPU mobiles d'aujourd'hui) ... eh bien, elles ont en fait simplement renvoyé de mauvaises données. (Ils ont ignoré les bits de poids faible.)

Enfin, notez que les lignes de cache peuvent être arbitrairement grandes et que le compilateur n'essaie pas de les deviner ou de faire un compromis espace / vitesse. Au lieu de cela, les décisions d'alignement font partie de l'ABI et représentent l'alignement minimum qui remplira éventuellement uniformément une ligne de cache.

TL; DR: l' alignement est important.

DigitalRoss
la source