web-dev-qa-db-fra.com

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

Duplicata possible:
Pourquoi GCC n'optimise-t-il pas les structures?
Pourquoi C++ ne rend-il pas la structure plus serrée?

Prenons l'exemple suivant sur une machine x86 32 bits:

En raison de 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 contre 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 la langue a été conçue de cette façon. 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 soucierait pas de la différence.

[~ # ~] modifier [~ # ~] : 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 seraient-ils toujours valables si le réarrangement faisait partie du langage? Disons qu'il y avait une règle de réarrangement spécifiée, à partir de laquelle nous avons exigé au moins que

  1. nous ne devons réorganiser la structure que si cela est réellement 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'il 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 (c'est-à-dire que la règle est fixe)

En abordant 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 dans un style serré (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 là, vous pouvez l'insérer manuellement en utilisant des variables factices, et/ou il pourrait y avoir des mots-clés/directives.

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

  • Optimisation: Tout l'intérêt de la réorganisation est l'optimisation (mémoire). Je vois beaucoup de potentiel ici. Nous ne pourrons peut-être pas supprimer tous les rembourrages ensemble, mais je ne vois pas comment la réorganisation pourrait limiter l'optimisation de quelque manière que ce soit.

  • Type casting: Il me semble que c'est le plus gros problème. Pourtant, il devrait y avoir des moyens de contourner cela. Étant donné que les règles sont fixes dans la langue, 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 se cassera jamais.

La raison pour laquelle je pense qu'une telle règle pourrait avoir un sens est parce que je trouve plus naturel de regrouper les membres de la structure par leur contenu 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 disposition optimale peut même être celle que je ne peux pas exprimer de manière sûre. D'un autre côté, cela semble rendre le langage plus compliqué, ce qui est bien sûr un inconvénient.

Notez que je ne parle pas de changer la langue - seulement si elle aurait pu (/ aurait dû) être conçue différemment.

Je sais que ma question est hypothétique, mais je pense que la discussion donne un aperçu plus approfondi des niveaux inférieurs de la conception de la machine et du langage.

Je suis assez nouveau ici, donc je ne sais pas si je devrais engendrer une nouvelle question à ce sujet. Veuillez me dire si c'est le cas.

87
Halle Knast

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

  • Le compilateur C ne sait pas si le struct représente la structure mémoire des objets 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 binaire des données est également définie dans un endroit inaccessible au compilateur, donc la réorganisation des champs struct créerait un type de données qui n'est pas cohérent avec les autres définitions. Par exemple, le en-tête d'un fichier dans un fichier Zip contient plusieurs champs 32 bits mal alignés. La réorganisation des champs empêcherait le code C de lire ou d'écrire directement l'en-tête (en supposant que l'implémentation Zip souhaite accéder directement aux données):

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

    L'attribut packed empêche le compilateur d'aligner les champs en fonction de leur alignement naturel, et il n'a aucun rapport avec le problème de l'ordre des champs. Il serait possible de réorganiser les champs de LocalFileHeader afin que la structure ait à la fois une 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);
    

    Il s'agit d'un modèle de programmation de bas niveau largement utilisé , en particulier si l'en-tête contient l'ID de type de données situé juste au-delà de l'en-tête.

  • Si un type struct est incorporé dans un autre type struct, il est impossible d'aligner le struct intérieur:

    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;
    };
    
  • Étant donné que la plupart des compilateurs C optimisent les compilateurs, la réorganisation des champs de structure nécessiterait de nouvelles optimisations pour être implémentées. Il est douteux que ces optimisations soient capables de faire mieux que ce que les programmeurs sont capables d'é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 des registres, la fonction inline, le pliage constant, la transformation d'une instruction switch en binaire recherche, etc. Ainsi, les avantages à gagner en permettant au compilateur d'optimiser les structures de données semblent moins tangibles que les optimisations traditionnelles du compilateur.

70
user811773

C est conçu et destiné à permettre d'écrire du matériel non portable et du code dépendant du format dans un langage de haut niveau. Le réarrangement du contenu de la structure derrière le dos du programmateur détruirait cette capacité.

Observez ce code réel à partir de l'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 sa présentation à l'en-tête d'un datagramme IP. Il est utilisé pour interpréter directement les taches de mémoire incrustées par un contrôleur Ethernet comme des en-têtes de datagramme IP. Imaginez que le compilateur réorganise 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 donnée via le __packed macro) mais ce n'est pas la question. C est spécifiquement conç pour permettre d'écrire du code de haut niveau non portable pour piloter du matériel. C'est sa fonction dans la vie.

29
Perry

C [et C++] sont considérés comme des langages de programmation de systèmes, ils fournissent donc un accès 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 à divers 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;
11
perreal

N'étant pas membre du WG14, 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 sacrée bonne raison pour laquelle je souhaite disposer mes éléments dans un ordre spécifique, qu'il soit le plus économe en espace ou non, et je ne voudrais pas que le compilateur réorganise ces éléments;

  2. Il a le potentiel de casser une quantité non triviale de code existant - il y a beaucoup de code hérité 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 classique code qui a fait cette hypothèse);

C99 Rationale traite directement le deuxième point ("Le code existant est important, les implémentations existantes ne le sont pas") et indirectement le premier ("Faites confiance au programmeur").

10
John Bode

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

9
vicatcu

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

6
larsks

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 choses d'un tour à ce niveau.

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

5
Peter M

Gardez à l'esprit 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 est également disponible pour d'autres compilateurs comme représentant ce type de données. Il se retrouvera probablement dans un fichier .h. Par conséquent, si un compilateur va prendre des libertés avec la façon 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.

4
Kluge

Votre cas est très spécifique car il nécessiterait que le premier élément d'un struct soit remis en ordre. Ce n'est pas possible, car l'élément défini en premier dans un struct doit toujours être à l'offset 0. Beaucoup de (faux) codes se briseraient si cela était autorisé.

Plus généralement, les pointeurs de sous-objets qui vivent à l'intérieur du même objet plus grand doivent toujours permettre une comparaison de pointeurs. Je peux imaginer qu'un code qui utilise cette fonctionnalité se briserait si vous inversiez l'ordre. Et pour cette comparaison, la connaissance du compilateur au point de définition n'aiderait pas: un pointeur vers un sous-objet n'a pas de "marque" à quel objet plus grand il appartient. Lorsqu'elles sont transmises à une autre fonction en tant que telles, toutes les informations d'un contexte possible sont perdues.

2
Jens Gustedt

Voici une raison que je n'ai pas vue jusqu'à présent - sans règles de réarrangement standard, cela briserait 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, puis liés. La compilation peut être à des moments différents (peut-être que vous en avez touché un seul, il a donc dû être recompilé), peut-être sur différents ordinateurs (si les fichiers se trouvent sur un lecteur réseau) ou même sur différentes versions du compilateur.
Si à un moment donné, le compilateur décidait de réorganiser, et à un autre il ne le ferait pas, les deux fichiers ne s'entendraient pas sur l'emplacement des champs.

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

2
ugoren

supposons que vous avez un en-tête a.h avec

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

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

si le compilateur est autorisé à réorganiser les membres comme bon lui semble ce sera impossible comme le compilateur client ne sait pas s'il faut utiliser la structure telle quelle ou optimisée (et puis b va devant ou derrière) ou même complètement rembourrée 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 qui nécessite que tous les compilateurs l'implémentent et que l'algorithme soit bon (en termes d'efficacité). il est plus facile de simplement convenir des règles de remplissage que de la réorganisation

il est facile d'ajouter un #pragma qui interdit l'optimisation lorsque vous avez besoin que la disposition d'une structure spécifique soit exactement ce dont vous avez besoin, donc ce n'est pas un problème

1
ratchet freak