Pourquoi les compilateurs C ne peuvent-ils pas réorganiser les membres de la structure pour éliminer le remplissage d’alignement?

Duplication possible:
Pourquoi GCC n’optimise-t-il pas les structures?
Pourquoi C ++ ne renforce-t-il pas la structure?

Prenons l’exemple suivant sur un ordinateur 32 bits x86:

En raison des contraintes d’alignement, la structure suivante

struct s1 { char a; int b; char c; char d; char e; } 

pourrait être représenté plus efficacement en mémoire (12 vs 8 octets) si les membres étaient réorganisés comme dans

 struct s2 { int b; char a; char c; char d; char e; } 

Je sais que les compilateurs C / C ++ ne sont pas autorisés à le faire. Ma question est pourquoi le langage a été conçu de cette manière. Après tout, nous pourrions finir par gaspiller de grandes quantités de mémoire, et des références telles que struct_ref->b ne se soucieraient pas de la différence.

EDIT : Merci à tous pour vos réponses extrêmement utiles. Vous expliquez très bien pourquoi le réarrangement ne fonctionne pas à cause de la façon dont le langage a été conçu. Cependant, cela me fait penser: ces arguments restraient-ils valables si le réarrangement faisait partie de la langue? Disons qu’il y avait une règle de réarrangement spécifique, à partir de laquelle nous avons exigé au moins que

  1. nous devrions seulement réorganiser la structure si nécessaire (ne faites rien si la structure est déjà “serrée”)
  2. la règle ne regarde que la définition de la structure, pas à l’intérieur des structures internes. Cela garantit qu’un type de structure a la même disposition, qu’elle soit interne ou non dans une autre structure.
  3. la disposition de la mémoire compilée d’une structure donnée est prévisible compte tenu de sa définition (la règle est fixe)

Adressant vos arguments un par un, je raisonne:

  • Mappage de données de bas niveau, “élément de moindre surprise” : écrivez vous-même vos structures (comme dans la réponse de @ Perry) et rien n’a changé (exigence 1). Si, pour une raison étrange, vous voulez que le remplissage interne soit présent, vous pouvez l’insérer manuellement à l’aide de variables factices, et / ou il pourrait y avoir des mots-clés / directives.

  • Différences du compilateur : L’exigence 3 élimine cette préoccupation. En fait, d’après les commentaires de @David Heffernan, il semble que nous ayons ce problème aujourd’hui parce que différents compilateurs remplissent différemment?

  • Optimisation : L’intérêt de la réorganisation est l’optimisation de la mémoire. Je vois beaucoup de potentiel ici. Nous ne pourrons peut-être pas supprimer tous les paddings, mais je ne vois pas en quoi la réorganisation pourrait limiter l’optimisation.

  • Type casting : Il me semble que c’est le plus gros problème. Cependant, il devrait y avoir des moyens de contourner ce problème. Comme les règles sont fixes dans le langage, le compilateur est capable de comprendre comment les membres ont été réorganisés et de réagir en conséquence. Comme mentionné ci-dessus, il sera toujours possible d’empêcher la réorganisation dans les cas où vous souhaitez un contrôle complet. En outre, l’exigence 2 garantit que le code de type sécurisé ne sera jamais rompu.

La raison pour laquelle je pense qu’une telle règle pourrait avoir un sens est que je trouve plus naturel de regrouper les membres de la structure par leur contenu plutôt que par leur type. De plus, il est plus facile pour le compilateur de choisir le meilleur ordre que pour moi quand j’ai beaucoup de structures internes. La mise en page optimale peut même être celle que je ne peux pas exprimer de manière sûre. En revanche, cela semblerait rendre la langue plus compliquée, ce qui constitue bien sûr un inconvénient.

Notez que je ne parle pas de changer la langue – seulement si elle pourrait (/ devrait) avoir été conçue différemment.

Je sais que ma question est hypothétique, mais je pense que la discussion permet de mieux comprendre les niveaux inférieurs de la conception de la machine et du langage.

Je suis assez nouveau ici, alors je ne sais pas si je devrais lancer une nouvelle question pour cela. S’il vous plaît dites-moi si c’est le cas.

Il existe plusieurs raisons pour lesquelles le compilateur C ne peut pas réorganiser automatiquement les champs:

  • Le compilateur C ne sait pas si la struct représente la structure mémoire des objects au-delà de l’unité de compilation actuelle (par exemple: une bibliothèque étrangère, un fichier sur disque, des données réseau, des tables de pages CPU, …). Dans un tel cas, la structure binary des données est également définie dans un endroit inaccessible au compilateur, donc la réorganisation des champs de la struct créerait un type de données incompatible avec les autres définitions. Par exemple, l’en- tête d’un fichier dans un fichier ZIP contient plusieurs champs 32 bits mal alignés. Réorganiser les champs rend impossible la lecture ou l’écriture directe de l’en-tête par le code C (en supposant que l’implémentation ZIP souhaite accéder directement aux données):

     struct __atsortingbute__((__packed__)) LocalFileHeader { uint32_t signature; uint16_t minVersion, flag, method, modTime, modDate; uint32_t crc32, compressedSize, uncompressedSize; uint16_t nameLength, extraLength; }; 

    L’atsortingbut packed empêche le compilateur d’aligner les champs en fonction de leur alignement naturel et n’a aucun rapport avec le problème de la mise en ordre des champs. Il serait possible de réorganiser les champs de LocalFileHeader afin que la structure ait la taille minimale et que tous les champs soient alignés sur leur alignement naturel. Cependant, le compilateur ne peut pas choisir de réorganiser les champs car il ne sait pas que la structure est réellement définie par la spécification du fichier ZIP.

  • C est un langage dangereux. Le compilateur C ne sait pas si les données seront accessibles via un type différent de celui vu par le compilateur, par exemple:

     struct S { char a; int b; char c; }; struct S_head { char a; }; struct S_ext { char a; int b; char c; int d; char e; }; struct S s; struct S_head *head = (struct S_head*)&s; fn1(head); struct S_ext ext; struct S *sp = (struct S*)&ext; fn2(sp); 

    Ceci est un modèle de programmation de bas niveau très répandu , surtout si l’en-tête contient le type d’ID des données situées juste au-delà de l’en-tête.

  • Si un type de struct est incorporé dans un autre type de struct , il est impossible d’intégrer la struct interne:

     struct S { char a; int b; char c, d, e; }; struct T { char a; struct S s; // Cannot inline S into T, 's' has to be compact in memory char b; }; 

    Cela signifie également que le déplacement de certains champs de S vers une structure distincte désactive certaines optimisations:

     // Cannot fully optimize S struct BC { int b; char c; }; struct S { char a; struct BC bc; char d, e; }; 
  • Comme la plupart des compilateurs C optimisent les compilateurs, la réorganisation des champs de structure nécessiterait l’implémentation de nouvelles optimisations. On peut se demander si ces optimisations pourraient faire mieux que ce que les programmeurs peuvent écrire. La conception manuelle des structures de données prend beaucoup moins de temps que les autres tâches du compilateur telles que l’allocation de registre, l’insertion de fonctions, le repliement constant, la transformation d’un énoncé en recherche binary, etc. semblent être moins tangibles que les optimisations traditionnelles du compilateur.

C est conçu et conçu pour permettre l’écriture de matériel non portable et d’un code dépendant du format dans un langage de haut niveau. La réorganisation du contenu de la structure à l’arrière du programmeur détruirait cette capacité.

Observez ce code depuis ip.h de NetBSD:

 /* * Structure of an internet header, naked of options. */ struct ip { #if BYTE_ORDER == LITTLE_ENDIAN unsigned int ip_hl:4, /* header length */ ip_v:4; /* version */ #endif #if BYTE_ORDER == BIG_ENDIAN unsigned int ip_v:4, /* version */ ip_hl:4; /* header length */ #endif u_int8_t ip_tos; /* type of service */ u_int16_t ip_len; /* total length */ u_int16_t ip_id; /* identification */ u_int16_t ip_off; /* fragment offset field */ u_int8_t ip_ttl; /* time to live */ u_int8_t ip_p; /* protocol */ u_int16_t ip_sum; /* checksum */ struct in_addr ip_src, ip_dst; /* source and dest address */ } __packed; 

Cette structure est identique dans la présentation à l’en-tête d’un datagramme IP. Il est utilisé pour interpréter directement les gouttes de mémoire intégrées dans un contrôleur Ethernet en tant qu’en-têtes de datagrammes IP. Imaginez si le compilateur réarrangeait arbitrairement le contenu sous l’auteur – ce serait un désastre.

Et oui, ce n’est pas précisément portable (et il y a même une directive gcc non portable fournie via la macro __packed ) mais ce n’est pas le cas. C est spécifiquement conçu pour permettre l’écriture de code de haut niveau non portable pour la conduite de matériel. C’est sa fonction dans la vie.

C [et C ++] sont considérés comme des langages de programmation système, de sorte qu’ils fournissent un access de bas niveau au matériel, par exemple la mémoire au moyen de pointeurs. Le programmeur peut accéder à un bloc de données et le convertir en une structure et accéder à différents membres [facilement].

Un autre exemple est une structure comme celle ci-dessous, qui stocke des données de taille variable.

 struct { uint32_t data_size; uint8_t data[1]; // this has to be the last member } _vv_a; 

N’étant pas membre du GT14, je ne peux rien dire de définitif, mais j’ai mes propres idées:

  1. Cela violerait le principe de la moindre surprise – il peut y avoir une bonne raison pour que je veuille exposer mes éléments dans un ordre spécifique, que ce soit ou non l’espace le plus efficace, et je ne voudrais pas que le compilateur réorganise ces éléments;

  2. Il a le potentiel de casser une quantité non négligeable de code existant – il y a beaucoup de code existant qui repose sur des choses comme l’adresse de la structure étant la même que l’adresse du premier membre (vu beaucoup de MacOS classiques). code qui a fait cette supposition);

La justification C99 aborde directement le deuxième point (“Le code existant est important, les implémentations existantes ne le sont pas”) et adresse indirectement le premier (“Faites confiance au programmeur”).

Cela changerait la sémantique des opérations de pointeur pour réorganiser les membres de la structure. Si vous êtes soucieux de la représentation de la mémoire compacte, en tant que programmeur, vous avez la responsabilité de connaître votre architecture cible et d’organiser vos structures en conséquence.

Si vous lisiez / écriviez des données binarys vers / depuis des structures C, la réorganisation des membres de la struct serait un désastre. Il n’y aurait pas de moyen pratique de remplir la structure à partir d’un tampon, par exemple.

Les structures sont utilisées pour représenter le matériel physique aux niveaux les plus bas. En tant que tel, le compilateur ne peut pas déplacer les objects d’un tour à l’autre à ce niveau.

Cependant, il ne serait pas déraisonnable d’avoir un #pragma qui permette au compilateur de réorganiser les structures purement basées sur la mémoire qui ne sont utilisées qu’en interne dans le programme. Cependant, je ne connais pas une telle bête (mais cela ne signifie pas squat – je suis déconnecté de C / C ++)

N’oubliez pas qu’une déclaration de variable, telle qu’une structure, est conçue pour être une représentation “publique” de la variable. Il est utilisé non seulement par votre compilateur, mais également par d’autres compilateurs représentant ce type de données. Il va probablement se retrouver dans un fichier .h. Par conséquent, si un compilateur doit prendre des libertés avec la manière dont les membres d’une structure sont organisés, alors TOUS les compilateurs doivent pouvoir suivre les mêmes règles. Sinon, comme cela a été mentionné, l’arithmétique du pointeur sera confondue entre différents compilateurs.

Voici une raison pour laquelle je n’ai pas vu jusqu’ici – sans règles de réarrangement standard, cela mettrait fin à la compatibilité entre les fichiers source.

Supposons qu’une structure soit définie dans un fichier d’en-tête et utilisée dans deux fichiers.
Les deux fichiers sont compilés séparément et liés ultérieurement. La compilation peut se faire à différents moments (peut-être en avez-vous touché un, il a donc dû être recompilé), éventuellement sur des ordinateurs différents (si les fichiers sont sur un lecteur réseau) ou même des versions de compilateur différentes.
Si à un moment donné, le compilateur décide de réorganiser, et dans un autre cas, les deux fichiers ne seront pas d’accord sur l’endroit où se trouvent les champs.

Par exemple, pensez à l’appel système et à la struct stat .
Lorsque vous installez Linux (par exemple), vous obtenez libC, qui inclut stat , qui a été compilé par quelqu’un d’autre.
Vous comstackz ensuite une application avec votre compilateur, avec vos indicateurs d’optimisation, et attendez-vous à ce que les deux s’accordent sur la disposition de la structure.

Votre cas est très spécifique car il faudrait que le premier élément d’une struct soit remis en ordre. Ce n’est pas possible, car l’élément défini en premier dans une struct doit toujours être au décalage 0 . Un lot de code (bidon) serait rompu si cela était autorisé.

Plus généralement, les pointeurs de sous-objects qui vivent dans le même object plus grand doivent toujours permettre la comparaison de pointeurs. Je peux imaginer qu’un code qui utilise cette fonctionnalité serait rompu si vous inversiez l’ordre. Et pour cette comparaison, la connaissance du compilateur au sharepoint définition ne serait pas utile: un pointeur vers un sous-object n’a pas de “marque” faisant l’object plus grand auquel il appartient. Lorsqu’elles sont transmises à une autre fonction, toutes les informations d’un contexte possible sont perdues.

supposons que vous ayez un en-tête ah avec

 struct s1 { char a; int b; char c; char d; char e; } 

et ceci fait partie d’une bibliothèque séparée (dont vous n’avez que les binarys compilés compilés par un compilateur inconnu) et que vous souhaitez utiliser cette structure pour communiquer avec cette bibliothèque,

Si le compilateur est autorisé à réorganiser les membres comme bon lui semble, cela sera impossible, car le compilateur client ne sait pas s’il doit utiliser la structure telle quelle ou si elle est optimisée (et si b passe devant ou derrière) ou même entièrement rempli avec chaque membre aligné sur des intervalles de 4 octets

pour résoudre ce problème, vous pouvez définir un algorithme déterministe pour le compactage, mais cela nécessite que tous les compilateurs l’implémentent et que l’algorithme soit bon (en termes d’efficacité). il est plus facile de se mettre d’accord sur les règles de remplissage que sur la réorganisation

il est facile d’append un #pragma qui interdit l’optimisation lorsque vous avez besoin de la disposition d’une structure spécifique, c’est exactement ce dont vous avez besoin, ce qui ne pose aucun problème