39 votes

Lors de l'extension d'une structure rembourrée, pourquoi des champs supplémentaires ne peuvent-ils pas être placés dans le rembourrage de la queue ?

Considérons les structs :

struct S1 {
    int a;
    char b;
};

struct S2 {
    struct S1 s;       /* struct needed to make this compile as C without typedef */
    char c;
};

// For the C++ fans
struct S3 : S1 {
    char c;
};

La taille de S1 est de 8, ce qui est attendu en raison de l'alignement. Mais la taille de S2 et S3 est de 12. Ce qui signifie que le compilateur les structure comme :

| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11|
|       a       | b |  padding  | c |  padding  |

Le compilateur pourrait placer c dans le remplissage en 6 7 8 sans rompre les contraintes d'alignement. Quelle est la règle qui l'en empêche, et quelle en est la raison ?

0 votes

Cela ne signifie certainement pas que le compilateur l'a conçu de cette manière.

2 votes

Il faut encore l'aligner correctement lorsque vous faites un tableau de ces structures. Cela nécessite un remplissage supplémentaire à la fin.

0 votes

@HansPassant : Cela n'explique pas pourquoi S3 ne peut pas tenir dans 8 octets...

22voto

Kerrek SB Points 194696

Réponse courte (pour la partie C++ de la question) : Le site ABI Itanium pour C++ interdit, pour des raisons historiques, d'utiliser le rembourrage de queue d'un sous-objet de base de type POD. Notez que C++11 ne comporte pas une telle interdiction. La règle pertinente 3.9/2 qui permet aux types trivialement copiables d'être copiés via leur représentation sous-jacente exclut explicitement les sous-objets de base.


Longue réponse : Je vais essayer de traiter C++11 et C en même temps.

  1. La disposition des S1 doit inclure un rembourrage, puisque S1::a doivent être alignés pour int et un tableau S1[N] consiste en des objets alloués de manière contiguë de type S1 chacun d'entre eux a Le membre doit être tellement aligné.
  2. En C++, les objets d'un type trivialement copiable T qui ne sont pas des sous-objets de base peuvent être traités comme des tableaux de sizeof(T) (c'est-à-dire que vous pouvez convertir un pointeur d'objet en un octet de type unsigned char * et de traiter le résultat comme un pointeur vers le premier élément d'un fichier unsigned char[sizeof(T)] et la valeur de ce tableau détermine l'objet). Puisque tous les objets en C sont de ce type, ceci explique que S2 pour C et C++.
  3. Les cas intéressants restant pour le C++ sont :
    1. les sous-objets de base, qui ne sont pas soumis à la règle ci-dessus (cf. C++11 3.9/2), et
    2. tout objet qui n'est pas de type trivialement copiable.

Pour 3.1, il existe en effet des "optimisations de disposition de base" courantes et populaires dans lesquelles les compilateurs "compressent" les membres de données d'une classe dans les sous-objets de base. Ceci est plus frappant lorsque la classe de base est vide (∞% de réduction de taille !), mais s'applique plus généralement. Cependant, l'ABI Itanium pour C++ dont j'ai donné le lien plus haut et que de nombreux compilateurs mettent en œuvre interdit une telle compression de remplissage de queue lorsque le type de base respectif est POD (et POD signifie trivialement copiable et standard-layout).

Pour la version 3.2, la même partie de l'ABI de l'Itanium s'applique, bien que je ne pense pas que la norme C++11 rende obligatoire l'utilisation d'un code source arbitraire, non trivialement copiable. membre doivent avoir la même taille qu'un objet complet du même type.


La réponse précédente est conservée comme référence.

Je crois que c'est parce que S1 est une mise en page standard, et donc, pour une raison quelconque, l'option S1 -subjectif de S3 reste intacte. Je ne suis pas sûr que ce soit mandaté par la norme.

Cependant, si nous nous tournons S1 dans une disposition non standard, nous observons une optimisation de la disposition :

struct EB { };

struct S1 : EB {   // not standard-layout
    EB eb;
    int a;
    char b;
};

struct S3 : S1 {
    char c;
};

Maintenant sizeof(S1) == sizeof(S3) == 12 sur ma plateforme. Démonstration en direct .

Et voici un Un exemple plus simple :

struct S1 {
private:
    int a;
public:
    char b;
};

struct S3 : S1 {
    char c;
};

L'accès mixte rend S1 une mise en page non standard. (Maintenant sizeof(S1) == sizeof(S3) == 8 .)

Mise à jour : Le facteur déterminant semble être trivialité ainsi que la standard-layoutness, c'est-à-dire que la classe doit être POD. La classe non-POD standard-layout suivante est optimisable base-layout :

struct S1 {
    ~S1(){}
    int a;
    char b;
};

struct S3 : S1 {
    char c;
};

Encore une fois sizeof(S1) == sizeof(S3) == 8 . Démo

0 votes

Quelqu'un dispose-t-il de la norme C++03 ? Il est possible que les compilateurs essaient de maintenir une certaine compatibilité avec C++03, qui avait une classification différente des types de classe et d'union et des types de classe différents. memcpy règles.

0 votes

Cela n'explique toujours pas pourquoi, mais montre au moins que je ne suis pas fou, qu'une telle disposition est possible et a du sens, et explique même dans quelles conditions cette disposition sera utilisée ou non. Accepté. Toujours désireux de savoir pourquoi ces règles sont ce qu'elles sont.

1 votes

Cela ne répond pas du tout à la partie C de la question, qui concerne uniquement le fait que la taille d'un type ne peut pas changer en fonction du contexte dans lequel il apparaît, et le fait que les structures peuvent être utilisées dans des tableaux.

19voto

sharth Points 25625

Considérons un peu de code :

struct S1 {
    int a;
    char b;
};

struct S2 {
    S1 s;
    char c;
};

Considérons ce qui se passerait si sizeof(S1) == 8 y sizeof(S2) == 8 .

struct S2 s2;
struct S1 *s1 = &(s2.s);
memset(s1, 0, sizeof(*s1));

Vous avez maintenant écrasé S2::c .


Pour des raisons d'alignement du tableau, S2 ne peuvent pas non plus avoir une taille de 9, 10 ou 11. La prochaine taille valide est donc la 12.

1 votes

Cela ne devient un argument que si vous pouvez justifier d'une manière ou d'une autre que l'on devrait pouvoir utiliser memset pour accéder à la représentation d'un objet. J'imagine une sorte de trivially_copyable o standard_layout doit s'appliquer.

0 votes

@KerrekSB : Je pars du point de vue du C, où tout a une disposition standard (du moins je crois que c'est vrai).

0 votes

Eh bien, le C n'a pas d'héritage, donc la seule partie intéressante de la question (disposition du sous-objet de base) ne s'applique pas. Cela aurait un sens si le C++ avait une sorte de règle de compatibilité, mais je ne sais pas ce qu'elle est.

4voto

Michael Burr Points 181287

Voici quelques exemples de raisons pour lesquelles un compilateur ne peut pas placer un membre c dans le rembourrage arrière du struct S1 membre s . Supposons pour la suite que le compilateur ait placé struct S2.c dans le rembourrage de la struct S1.s. membre :

struct S1 {
    int a;
    char b;
};

struct S2 {
    struct S1 s;       /* struct needed to make this compile as C without typedef */
    char c;
};

// ...

struct S1 foo = { 10, 'a' };
struct S2 bar = {{ 20, 'b'}, 'c' };

bar.s = foo;    // this will likely corrupt bar.c

memcpy(&bar.s, &foo, sizeof(bar.s));    // this will certainly corrupt bar.c

bar.s.b = 'z';  // this is permited to corrupt bar by C99 6.2.6.1/6

C99/C11 6.2.6.1/6 ("Representation of types/general") dit :

Lorsqu'une valeur est stockée dans un objet de type structure ou union, y compris dans un objet membre, les octets de la représentation de l'objet qui correspondent à des octets de remplissage prennent des valeurs non spécifiées.

0 votes

Cela n'explique pas vraiment pourquoi, mais ajoute certainement à la discussion. Merci pour la référence à la norme.

0 votes

...à condition qu'il ne soit utilisé que pour accéder au membre pour lequel il a été aligné ; un pointeur qui satisfait à l'alignement "préféré" d'une union, en revanche, peut être utilisé pour accéder à tous les membres.

0voto

haccks Points 33022

Quelle est la raison du remplissage supplémentaire dans les structs ?

Si le processeur est sérieux au sujet alignement cela soulève une exception/un signal, sinon il y aura une pénalité de performance car le désalignement ralentit l'accès aux données.

Pour comprendre cela, commençons par alignement des structures de données :

L'alignement des structures de 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 questions distinctes mais liées : alignement des données y rembourrage de la structure de données . Lorsqu'un ordinateur moderne lit ou écrit à partir d'une adresse mémoire, il le fait par morceaux de la taille d'un mot (par exemple, des morceaux de 4 octets sur un système 32 bits) ou plus. L'alignement des données consiste à placer les données à un décalage mémoire égal à 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 quelques octets sans signification entre la fin de la dernière structure de données et le début de la suivante, ce qui constitue un remplissage de la structure de données.

Par exemple, si la taille des mots de l'ordinateur est de 4 octets (un octet signifie 8 bits sur la plupart des machines, mais peut être différent sur certains systèmes), les données à lire doivent se trouver à un décalage mémoire qui est un multiple de 4. Lorsque ce n'est pas le cas, par exemple si les données commencent à l'endroit où se trouve l'ordinateur. 14 au lieu de l'octet 16 l'ordinateur doit alors lire deux morceaux de 4 octets et effectuer certains calculs avant que les données demandées aient été lues, ou il peut générer un défaut d'alignement. . Même si la structure de données précédente se termine au 13e octet, la structure de données suivante doit commencer au 16e octet. Deux octets de remplissage sont insérés entre les deux structures de données pour aligner la structure de données suivante sur le 16e octet.


Lors de l'extension d'une structure rembourrée, pourquoi des champs supplémentaires ne peuvent-ils pas être placés dans le rembourrage de la queue ?

Le compilateur pourrait placer c dans le remplissage en 6 7 8 sans rompre les contraintes d'alignement. Quelle est la règle qui l'en empêche, et quelle en est la raison ?

Le compilateur pourrait le placer à cet endroit, mais l'accès à la mémoire de l'ordinateur serait alors limité. c sera maltraité 1 et il y aura une pénalité de performance Comme expliqué ci-dessus, pour organiser un tableau :

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

Cette structure aurait une taille compilée de 6 octets sur un système 32 bits.
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.


<strong>1 </strong>Un accès à la mémoire est dit aligné lorsque la donnée à laquelle on accède est <code>n</code> octets de long (où n est une puissance de 2) et l'adresse de la donnée est <code>n</code> -aligné sur l'octet. Lorsqu'un accès à la mémoire n'est pas aligné, on dit qu'il est mal aligné.

0 votes

Je suis bien conscient de l'alignement des données. Ce n'est pas le sujet de la question. J'ai abordé ce point spécifiquement, dans la question et dans les commentaires.

1 votes

J'ai lu la question trois fois et j'ai constaté qu'il vous manque quelque chose dans l'alignement des données. Lisez la deuxième partie de la réponse.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X