web-dev-qa-db-fra.com

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

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

629
Kevin

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

  • Un accès mal aligné peut être une erreur difficile (souvent SIGBUS).
  • Un accès mal aligné peut être une erreur logicielle.
    • Soit corrigé en matériel, pour une dégradation des performances modeste.
    • Ou corrigée par émulation dans un logiciel, pour une dégradation grave des performances.
    • En outre, l’atmicité et d’autres garanties de concurrence pourraient être rompues, ce qui entraînerait des erreurs subtiles.

Voici un exemple d'utilisation des paramètres typiques d'un processeur x86 (tous utilisaient les modes 32 et 64 bits):

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 de base) (comme la structure Z dans l'exemple ci-dessus).

REMARQUE IMPORTANTE: les normes C et C++ indiquent que l'alignement de 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, lorsque vous utilisez des 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 instructions spéciales #pragma pour modifier les paramètres d'alignement de la structure.

606
Kevin

L'emballage et l'alignement des octets, comme décrit dans le C FAQ ici :

C'est pour l'alignement. De nombreux processeurs ne peuvent pas accéder aux quantités de 2 et 4 octets (par exemple, in et long ints) 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 d’emballer cette structure dans la mémoire comme ceci:

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

Mais c’est beaucoup, beaucoup plus facile pour le processeur si le compilateur s’organise comme suit:

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

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

+-------+-------+-------+-------+
|           a           | pad1  |
+-------+-------+-------+-------+
|       b       |     pad2      |
+-------+-------+-------+-------+
|               c               |
+-------+-------+-------+-------+
|           d           | pad3  |
+-------+-------+-------+-------+
166
EmmEff

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 .

Il est généralement plus facile pour la CPU d’accéder à des données multiples de 4 (ou 8), selon la plate-forme et également sur le compilateur.

Il s’agit donc d’une question d’alignement.

Vous devez avoir de bonnes raisons pour le changer.

23
INS

Cela peut être dû à l'alignement et au remplissage d'octets afin que la structure contienne 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) est 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 - la dernière structure étant complétée de manière à ce qu'il s'agisse d'un mot unique (4 x 8 octets sur ma plate-forme 32 bits).

oneInt=4
twoInts=8
someBits=4
13
Kyle Burton

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 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 des membres en C++ . Les compilateurs peuvent (et le font certainement) 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 garantie par la norme (la mise en œuvre d’un mécanisme virtuel n’est pas précisée) et on peut donc en conclure que cette garantie est tout simplement impossible.

Je suis tout à fait sûr ordre-membre est ​​garanti en C , mais je ne compterais pas dessus, en écrivant un programme multi-plateforme ou multi-compilateur.

9
lkanab

La taille d'une structure est supérieure à la somme de ses parties en raison de ce que l'on appelle l'emballage. Un processeur particulier a une taille de données préférée avec laquelle il fonctionne. La taille préférée des processeurs modernes est 32 bits (4 octets). L'accès à la mémoire lorsque les 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érons 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 limite de 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 zéro, les données sont donc alignées sur une limite de 32 bits). L'accès à data.a fonctionnera correctement car il commence sur une limite - 0x400. L'accès à data.b fonctionnera également correctement 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 un ordinateur 32 bits, le système lirait data.c pendant un cycle de mémoire, mais n'obtiendrait que 3 des 4 octets (le 4ème octet se trouve sur la limite suivante). Donc, le système devrait faire un deuxième accès en mémoire pour obtenir le 4ème octet,

Maintenant, si au lieu de mettre data.c à l'adresse 0x405, le compilateur complétait la structure de 3 octets et plaçait data.c à l'adresse 0x408, le système n'aurait alors besoin que d'un cycle pour lire les données, réduisant ainsi 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 disposer d’énormes quantités de mémoire (plusieurs gigaoctets), les compilateurs estiment que l’échange (vitesse supérieure à la taille) est raisonnable.

Malheureusement, ce problème devient mortel 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'autre part, différents compilateurs ont des capacités différentes pour gérer le compactage 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 compris entre 11 et 14 (et pour certains systèmes, jusqu'à 32), en fonction de la compression par défaut du compilateur.

6
sid1138

Cela peut se 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 ne représente pas un multiple de 4 octets.

De plus, une bibliothèque peut être compilée sous x86 avec des ints 32 bits et vous pouvez comparer ses composants sur un processus 64 bits vous donnerait un résultat différent si vous le faisiez à la main.

5
Orion Adrian

Projet de norme 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 ayant un type de structure ou d'union, le résultat est le nombre total d'octets contenus 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 non nommé dans un objet de structure, mais pas à son début.

et:

15 Il peut y avoir un remplissage non nommé à la fin d'une structure ou d'une union.

Le nouveau C99 fonctionnalité de membre de groupe flexible (struct S {int is[];};) peut également affecter le remplissage:

16 Comme cas particulier, le dernier élément d'une structure comportant plusieurs membres nommés peut avoir un type de tableau incomplet; c'est ce qu'on appelle un membre de groupe flexible. Dans la plupart des cas, le membre de groupe flexible est ignoré. En particulier, la taille de la structure est la même que si le membre de groupe flexible était omis, à l'exception du fait qu'il pourrait y avoir plus de remplissage de fin que ce que l'omission impliquerait.

Annexe J Problèmes 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 de norme 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 de la classe :

Un pointeur sur un objet struct standard-layout, converti de manière appropriée à l'aide d'un reinterpret_cast, pointe vers son membre initial (ou si ce membre est un champ de bits, puis vers l'unité dans laquelle il réside) et inversement. [Remarque: il peut donc y avoir un remplissage non nommé dans un objet struct standard-layout, 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 :-)

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.

4
JohnMcG

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. C’est parce 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 l'opérateur sizeof. Le sizeof n'inclut que la taille du tableau flexible, disponible en C/C++
  • Certaines implémentations du langage vous permettent de contrôler la disposition de la mémoire des structures via les options pragma et compilateur

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

  • les compilateurs doivent attribuer une séquence de composants augmentant les adresses de mémoire
  • L'adresse du premier composant coïncide avec l'adresse de début de la structure
  • des champs de bits non nommés peuvent être inclus dans la structure jusqu'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)

Comment fonctionne l'alignement:

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

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

3
bruziuz

L'idée est que, pour des raisons de rapidité et de cache, les opérandes doivent être lus à partir d'adresses alignées sur leur taille naturelle. Pour que cela se produise, les compilateurs compilent les membres de la structure afin que le membre suivant ou le suivant 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 été capable de récupérer des adresses mal alignées. Cependant, il est plus lent et lorsque le défaut d’alignement recouvre deux lignes de cache différentes, il en supprime deux alors qu’un accès aligné en exclurait une seule.

Certaines architectures doivent en effet intercepter des lectures et des écritures mal alignées, et les premières versions de l'architecture ARM (celle qui a évolué vers tous les processeurs mobiles actuels) ... eh bien, elles ne renvoient que de mauvaises données pendant ceux. (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 ni 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 permettant de remplir uniformément une ligne de cache.

TL; DR: l'alignement est important.

2
DigitalRoss