Rembourrage et emballage de structure

209

Considérer:

struct mystruct_A
{
   char a;
   int b;
   char c;
} x;

struct mystruct_B
{
   int b;
   char a;
} y;

Les tailles des structures sont respectivement 12 et 8.

Ces structures sont-elles rembourrées ou emballées?

Quand le rembourrage ou l'emballage a-t-il lieu?

Manu
la source
24
L'emballage de l'art perdu de la structure C - catb.org/esr/structure-packing
Paolo
paddingrend les choses plus grandes. packingrend les choses plus petites. Totalement différent.
smwikipedia

Réponses:

264

Le remplissage aligne les membres de la structure sur les limites d'adresse "naturelles" - par exemple, les intmembres auraient des décalages, qui sont mod(4) == 0sur une plate-forme 32 bits. Le remplissage est activé par défaut. Il insère les "lacunes" suivantes dans votre première structure:

struct mystruct_A {
    char a;
    char gap_0[3]; /* inserted by compiler: for alignment of b */
    int b;
    char c;
    char gap_1[3]; /* -"-: for alignment of the whole struct in an array */
} x;

L'emballage , d'autre part, empêche le compilateur de faire du remplissage - cela doit être explicitement demandé - sous GCC c'est __attribute__((__packed__)), donc ce qui suit:

struct __attribute__((__packed__)) mystruct_A {
    char a;
    int b;
    char c;
};

produirait une structure de taille 6sur une architecture 32 bits.

Remarque cependant: l'accès à la mémoire non alignée est plus lent sur les architectures qui le permettent (comme x86 et amd64), et est explicitement interdit sur les architectures à alignement strict comme SPARC.

Nikolai Fetissov
la source
2
Je me demande: l'interdiction de la mémoire non alignée sur l'étincelle signifie qu'elle ne peut pas traiter les tableaux d'octets habituels? L'emballage des structures, comme je le sais, est principalement utilisé dans la transmission (c'est-à-dire la mise en réseau) de données, lorsque vous devez convertir un tableau d'octets en une structure et être sûr qu'un tableau correspond à un champ de structure. Si l'étincelle ne peut pas faire ça, comment ceux qui travaillent du tout?!
Hi-Angel
14
C'est exactement pourquoi, si vous regardez les dispositions d'en-tête IP, UDP et TCP, vous verriez que tous les champs entiers sont alignés.
Nikolai Fetissov
17
Le "Lost Art of C Structure Packing" explique les optimisations du rembourrage et de l'emballage - catb.org/esr/structure-packing
Rob11311
3
Le premier membre doit-il venir en premier? Je pensais que l'arrangement dépend entièrement de l'implémentation et ne peut être invoqué (même d'une version à l'autre).
allyourcode
4
+ allyourcode Le standard garantit que l'ordre des membres sera préservé et que le premier membre commencera à 0 offset.
martinkunev
64

( Les réponses ci-dessus expliquent la raison assez clairement, mais ne semblent pas totalement claires sur la taille du rembourrage, donc, j'ajouterai une réponse en fonction de ce que j'ai appris de The Lost Art of Structure Packing , il a évolué pour ne pas se limiter à C, mais également applicable à Go, Rust. )


Alignement de la mémoire (pour struct)

Règles:

  • Avant chaque membre individuel, il y aura un remplissage afin de le faire démarrer à une adresse divisible par sa taille.
    par exemple sur un système 64 bits, intdoit commencer à l'adresse divisible par 4 et longpar 8 shortpar 2.
  • charet char[]sont spéciaux, peuvent être n'importe quelle adresse mémoire, donc ils n'ont pas besoin de remplissage avant eux.
  • Car struct, à part le besoin d'alignement pour chaque membre individuel, la taille de la structure entière elle-même sera alignée sur une taille divisible par la taille du plus grand membre individuel, en remplissant à la fin.
    Par exemple, si le plus grand membre de struct est longalors divisible par 8, intpuis par 4, shortpuis par 2.

Ordre des membres:

  • L'ordre des membres peut affecter la taille réelle de la structure, alors gardez cela à l'esprit. par exemple le stu_cet stu_dde l' exemple ci - dessous ont les mêmes membres, mais dans un ordre différent, et le résultat de différentes tailles pour les 2 struct.

Adresse en mémoire (pour struct)

Règles:

  • L'
    adresse Struct du système 64 bits commence à partir des (n * 16)octets. ( Vous pouvez voir dans l'exemple ci-dessous que toutes les adresses hexadécimales imprimées des structures se terminent par 0. )
    Raison : le plus grand membre de structure individuel possible est de 16 octets ( long double).
  • (Mise à jour) Si une structure contient uniquement uncharmembre as, son adresse peut commencer à n'importe quelle adresse.

Espace vide :

  • L'espace vide entre 2 structures pourrait être utilisé par des variables non-structures qui pourraient s'adapter. Par
    exemple, en test_struct_address()dessous, la variable xréside entre la structure adjacente get h.
    Peu importe qu'il xsoit déclaré, hl'adresse de ne changera pas, il xsuffit de réutiliser l'espace vide qui a été ggaspillé.
    Cas similaire pour y.

Exemple

( pour système 64 bits )

memory_align.c :

/**
 * Memory align & padding - for struct.
 * compile: gcc memory_align.c
 * execute: ./a.out
 */ 
#include <stdio.h>

// size is 8, 4 + 1, then round to multiple of 4 (int's size),
struct stu_a {
    int i;
    char c;
};

// size is 16, 8 + 1, then round to multiple of 8 (long's size),
struct stu_b {
    long l;
    char c;
};

// size is 24, l need padding by 4 before it, then round to multiple of 8 (long's size),
struct stu_c {
    int i;
    long l;
    char c;
};

// size is 16, 8 + 4 + 1, then round to multiple of 8 (long's size),
struct stu_d {
    long l;
    int i;
    char c;
};

// size is 16, 8 + 4 + 1, then round to multiple of 8 (double's size),
struct stu_e {
    double d;
    int i;
    char c;
};

// size is 24, d need align to 8, then round to multiple of 8 (double's size),
struct stu_f {
    int i;
    double d;
    char c;
};

// size is 4,
struct stu_g {
    int i;
};

// size is 8,
struct stu_h {
    long l;
};

// test - padding within a single struct,
int test_struct_padding() {
    printf("%s: %ld\n", "stu_a", sizeof(struct stu_a));
    printf("%s: %ld\n", "stu_b", sizeof(struct stu_b));
    printf("%s: %ld\n", "stu_c", sizeof(struct stu_c));
    printf("%s: %ld\n", "stu_d", sizeof(struct stu_d));
    printf("%s: %ld\n", "stu_e", sizeof(struct stu_e));
    printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));

    printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
    printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));

    return 0;
}

// test - address of struct,
int test_struct_address() {
    printf("%s: %ld\n", "stu_g", sizeof(struct stu_g));
    printf("%s: %ld\n", "stu_h", sizeof(struct stu_h));
    printf("%s: %ld\n", "stu_f", sizeof(struct stu_f));

    struct stu_g g;
    struct stu_h h;
    struct stu_f f1;
    struct stu_f f2;
    int x = 1;
    long y = 1;

    printf("address of %s: %p\n", "g", &g);
    printf("address of %s: %p\n", "h", &h);
    printf("address of %s: %p\n", "f1", &f1);
    printf("address of %s: %p\n", "f2", &f2);
    printf("address of %s: %p\n", "x", &x);
    printf("address of %s: %p\n", "y", &y);

    // g is only 4 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "g", "h", (long)(&h) - (long)(&g));

    // h is only 8 bytes itself, but distance to next struct is 16 bytes(on 64 bit system) or 8 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "h", "f1", (long)(&f1) - (long)(&h));

    // f1 is only 24 bytes itself, but distance to next struct is 32 bytes(on 64 bit system) or 24 bytes(on 32 bit system),
    printf("space between %s and %s: %ld\n", "f1", "f2", (long)(&f2) - (long)(&f1));

    // x is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between g & h,
    printf("space between %s and %s: %ld\n", "x", "f2", (long)(&x) - (long)(&f2));
    printf("space between %s and %s: %ld\n", "g", "x", (long)(&x) - (long)(&g));

    // y is not a struct, and it reuse those empty space between struts, which exists due to padding, e.g between h & f1,
    printf("space between %s and %s: %ld\n", "x", "y", (long)(&y) - (long)(&x));
    printf("space between %s and %s: %ld\n", "h", "y", (long)(&y) - (long)(&h));

    return 0;
}

int main(int argc, char * argv[]) {
    test_struct_padding();
    // test_struct_address();

    return 0;
}

Résultat d'exécution - test_struct_padding():

stu_a: 8
stu_b: 16
stu_c: 24
stu_d: 16
stu_e: 16
stu_f: 24
stu_g: 4
stu_h: 8

Résultat d'exécution - test_struct_address():

stu_g: 4
stu_h: 8
stu_f: 24
address of g: 0x7fffd63a95d0  // struct variable - address dividable by 16,
address of h: 0x7fffd63a95e0  // struct variable - address dividable by 16,
address of f1: 0x7fffd63a95f0 // struct variable - address dividable by 16,
address of f2: 0x7fffd63a9610 // struct variable - address dividable by 16,
address of x: 0x7fffd63a95dc  // non-struct variable - resides within the empty space between struct variable g & h.
address of y: 0x7fffd63a95e8  // non-struct variable - resides within the empty space between struct variable h & f1.
space between g and h: 16
space between h and f1: 16
space between f1 and f2: 32
space between x and f2: -52
space between g and x: 12
space between x and y: 12
space between h and y: 8

Ainsi, le début de l'adresse pour chaque variable est g: d0 x: dc h: e0 y: e8

entrez la description de l'image ici

Eric Wang
la source
4
Les "règles" ont en fait été très claires, je n'ai trouvé aucune règle simple nulle part. Merci.
Pervez Alam
2
@PervezAlam Le livre <The Lost Art of C Structure Packing>explique assez bien les règles, même s'il est un peu plus long que cette réponse. Le livre est disponible gratuitement en ligne: catb.org/esr/structure-packing
Eric Wang
Je vais essayer, mais est-ce limité à l'emballage de la structure? Juste des curiosités car j'ai aimé l'explication dans le livre.
Pervez Alam
1
@PervezAlam C'est un livre très court, principalement axé sur la technologie qui réduirait l'empreinte mémoire du programme c, il ne prend que plusieurs jours pour terminer la lecture.
Eric Wang
1
@ValidusOculus Oui, cela signifie aligné sur 16 octets.
Eric Wang
44

Je sais que cette question est ancienne et la plupart des réponses ici expliquent très bien le rembourrage, mais en essayant de le comprendre moi-même, je me suis dit que le fait d'avoir une image "visuelle" de ce qui se passait aidait.

Le processeur lit la mémoire en "morceaux" d'une taille définie (mot). Disons que le mot du processeur fait 8 octets. Il considérera la mémoire comme une grande rangée de blocs de construction de 8 octets. Chaque fois qu'il a besoin d'obtenir des informations de la mémoire, il atteint l'un de ces blocs et l'obtient.

Alignement des variables

Comme le montre l'image ci-dessus, peu importe où se trouve un caractère (1 octet de long), car il se trouvera dans l'un de ces blocs, nécessitant que le processeur ne traite qu'un seul mot.

Lorsque nous traitons des données supérieures à un octet, comme un entier de 4 octets ou un double de 8 octets, la façon dont elles sont alignées dans la mémoire fait une différence sur le nombre de mots qui devront être traités par le CPU. Si les blocs de 4 octets sont alignés de manière à ce qu'ils s'adaptent toujours à l'intérieur d'un bloc (l'adresse mémoire étant un multiple de 4), un seul mot devra être traité. Sinon, un bloc de 4 octets pourrait avoir une partie de lui-même sur un bloc et une partie sur un autre, nécessitant que le processeur traite 2 mots pour lire ces données.

La même chose s'applique à un double de 8 octets, sauf que maintenant il doit être dans un multiple d'adresse mémoire de 8 pour garantir qu'il sera toujours à l'intérieur d'un bloc.

Cela considère un traitement de texte à 8 octets, mais le concept s'applique à d'autres tailles de mots.

Le remplissage fonctionne en comblant les écarts entre ces données pour s'assurer qu'elles sont alignées avec ces blocs, améliorant ainsi les performances lors de la lecture de la mémoire.

Cependant, comme indiqué dans d'autres réponses, parfois l'espace compte plus que la performance elle-même. Peut-être que vous traitez beaucoup de données sur un ordinateur qui n'a pas beaucoup de RAM (l'espace de swap pourrait être utilisé mais il est BEAUCOUP plus lent). Vous pouvez organiser les variables dans le programme jusqu'à ce que le moins de remplissage soit effectué (comme cela a été largement illustré dans certaines autres réponses), mais si cela ne suffit pas, vous pouvez explicitement désactiver le remplissage, ce qui est ce qu'est l' emballage .

IanC
la source
3
Cela n'explique pas l'empaquetage de la structure mais il illustre très bien l'alignement des mots CPU.
David Foerster
L'avez-vous dessiné en peinture? :-)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@ CiroSantilli709 大 抓捕 六四 事件 法轮功, c'était sur gimp, mais je suppose que j'aurais gagné un peu de temps à le faire sur la peinture si haha
IanC
1
Encore mieux depuis l'open source (Y)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
21

L'emballage de structure supprime le rembourrage de structure, le rembourrage utilisé lorsque l'alignement est le plus important, l'emballage utilisé lorsque l'espace est le plus important.

Certains compilateurs prévoient #pragmade supprimer le remplissage ou de le compresser à n nombre d'octets. Certains fournissent des mots clés pour ce faire. Généralement, le pragma utilisé pour modifier le remplissage de la structure sera au format ci-dessous (dépend du compilateur):

#pragma pack(n)

Par exemple, ARM fournit le __packed mot clé pour supprimer le remplissage de la structure. Parcourez le manuel de votre compilateur pour en savoir plus à ce sujet.

Une structure compacte est donc une structure sans rembourrage.

Des structures généralement tassées seront utilisées

  • pour économiser de l'espace

  • formater une structure de données à transmettre sur le réseau en utilisant un certain protocole (ce n'est pas une bonne pratique bien sûr car vous devez
    gérer l'endianité)

user2083050
la source
5

Le rembourrage et l'emballage ne sont que deux aspects de la même chose:

  • l'emballage ou l'alignement est la taille à laquelle chaque membre est arrondi
  • le rembourrage est l'espace supplémentaire ajouté pour correspondre à l'alignement

Dans mystruct_A, en supposant un alignement par défaut de 4, chaque membre est aligné sur un multiple de 4 octets. Puisque la taille de charest 1, le remplissage pour aet cest 4 - 1 = 3 octets alors qu'aucun remplissage n'est requis pour int bce qui est déjà 4 octets. Il en va de même pour mystruct_B.

casablanca
la source
1

L'empaquetage de structure n'est effectué que lorsque vous demandez explicitement à votre compilateur d'emballer la structure. Le rembourrage est ce que vous voyez. Votre système 32 bits remplit chaque champ pour l'alignement des mots. Si vous aviez dit à votre compilateur de compresser les structures, elles auraient respectivement 6 et 5 octets. Mais ne fais pas ça. Ce n'est pas portable et fait que les compilateurs génèrent du code beaucoup plus lent (et parfois même bogué).

nmichaels
la source
1

Règles de rembourrage:

  1. Chaque membre de la structure doit être à une adresse divisible par sa taille. Le remplissage est inséré entre les éléments ou à la fin de la structure pour s'assurer que cette règle est respectée. Cela est fait pour un accès au bus plus facile et plus efficace par le matériel.
  2. Le remplissage à la fin de la structure est décidé en fonction de la taille du plus grand membre de la structure.

Pourquoi la règle 2: Considérez la structure suivante,

Struct 1

Si nous devions créer un tableau (de 2 structures) de cette structure, aucun remplissage ne sera requis à la fin:

Tableau Struct1

Par conséquent, la taille de struct = 8 octets

Supposons que nous devions créer une autre structure comme ci-dessous:

Struct 2

Si nous devions créer un tableau de cette structure, il y a 2 possibilités, du nombre d'octets de remplissage requis à la fin.

A. Si nous ajoutons 3 octets à la fin et l'alignons pour int et non pour Long:

Tableau Struct2 aligné sur int

B. Si nous ajoutons 7 octets à la fin et l'alignons pour Long:

Tableau Struct2 aligné sur Long

L'adresse de début du deuxième tableau est un multiple de 8 (c'est-à-dire 24). La taille de la structure = 24 octets

Par conséquent, en alignant l'adresse de début du tableau suivant de la structure sur un multiple du plus grand membre (c'est-à-dire si nous devions créer un tableau de cette structure, la première adresse du deuxième tableau doit commencer à une adresse qui est un multiple du plus grand membre de la structure. Ici, 24 (3 * 8)), nous pouvons calculer le nombre d'octets de remplissage requis à la fin.

AlphaGoku
la source
-1

L'alignement de la structure des données est la façon dont les données sont organisées et accessibles dans la mémoire de l'ordinateur. Il se compose de deux problèmes distincts mais liés: l'alignement des données et le remplissage de la structure des données . Lorsqu'un ordinateur moderne lit ou écrit dans une adresse mémoire, il le fait en blocs de taille de mot (par exemple, blocs de 4 octets sur un système 32 bits) ou plus. L'alignement des données signifie placer les données à une adresse mémoire égale à un multiple de la taille du mot, ce qui augmente les performances du système en raison de la façon dont le CPU gère la mémoire. Pour aligner les données, il peut être nécessaire d'insérer des octets sans signification entre la fin de la dernière structure de données et le début du suivant, qui est le remplissage de la structure de données.

  1. Afin d'aligner les données en mémoire, un ou plusieurs octets vides (adresses) sont insérés (ou laissés vides) entre des adresses mémoire qui sont allouées à d'autres membres de la structure lors de l'allocation mémoire. Ce concept est appelé rembourrage de structure.
  2. L'architecture d'un processeur d'ordinateur est telle qu'il peut lire un mot (4 octets dans un processeur 32 bits) de la mémoire à la fois.
  3. Pour profiter de cet avantage du processeur, les données sont toujours alignées sur un paquet de 4 octets, ce qui conduit à insérer des adresses vides entre les adresses des autres membres.
  4. En raison de ce concept de remplissage de structure en C, la taille de la structure n'est pas toujours la même que ce que nous pensons.
manoj yadav
la source
1
Pourquoi avez-vous besoin de créer un lien vers le même article 5 fois dans votre réponse? Veuillez ne conserver qu'un seul lien vers l'exemple. De plus, puisque vous créez un lien vers votre article, vous devez divulguer ce fait.
Artjom B.