902 votes

Quelle est la règle de l'aliasing strict ?

Lorsque vous posez des questions sur comportement indéfini courant en C les gens font parfois référence à la règle stricte de l'aliasing.
De quoi parlent-ils ?

13 votes

@Ben Voigt : Les règles d'aliasing sont différentes pour c++ et c. Pourquoi cette question est étiquetée avec c y c++faq .

8 votes

@MikeMB : Si vous vérifiez l'historique, vous verrez que j'ai gardé les balises telles qu'elles étaient à l'origine, malgré la tentative de certains autres experts de modifier la question sous les réponses existantes. De plus, la dépendance du langage et de la version est une partie très importante de la réponse à "Quelle est la règle d'aliasing stricte ?" et connaître les différences est important pour les équipes qui migrent du code entre C et C++, ou qui écrivent des macros à utiliser dans les deux.

7 votes

@Ben Voigt : En fait - pour autant que je puisse dire - la plupart des réponses ne concernent que le c et non le c++ ; de plus, la formulation de la question indique que l'accent est mis sur les règles du c (ou le PO n'était tout simplement pas conscient, qu'il y a une différence). Pour la plupart, les règles et l'idée générale sont les mêmes bien sûr, mais surtout, en ce qui concerne les unions, les réponses ne s'appliquent pas à c++. Je suis un peu inquiet que certains programmeurs c++ recherchent la règle d'aliasing stricte et supposent que tout ce qui est énoncé ici s'applique également à c++.

638voto

Doug T. Points 33360

Une situation typique dans laquelle vous rencontrez des problèmes d'aliasing strict est la superposition d'une structure (comme un message de périphérique/réseau) sur un tampon de la taille d'un mot de votre système (comme un pointeur à uint32_t ou uint16_t s). Lorsque vous superposez une structure sur un tel tampon, ou un tampon sur une telle structure par le biais d'un moulage de pointeur, vous pouvez facilement violer les règles strictes d'aliasing.

Ainsi, dans ce type de configuration, si je veux envoyer un message à quelque chose, je dois avoir deux pointeurs incompatibles pointant sur le même morceau de mémoire. Je pourrais alors coder naïvement quelque chose comme ceci :

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i = 0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

La règle stricte de l'aliasing rend cette configuration illégale : le déréférencement d'un pointeur qui aliène un objet qui n'est pas d'une catégorie type compatible ou l'un des autres types autorisés par C 2011 6.5 paragraphe 7 1 est un comportement non défini. Malheureusement, vous pouvez toujours coder de cette façon, peut-être obtenir quelques avertissements, avoir une bonne compilation, mais avoir un comportement bizarre et inattendu lorsque vous exécutez le code.

(GCC semble quelque peu incohérent dans sa capacité à donner des avertissements d'aliasing, nous donnant parfois un avertissement amical et parfois non).

Pour voir pourquoi ce comportement est indéfini, nous devons penser à ce que la règle d'aliasing strict achète au compilateur. Fondamentalement, avec cette règle, il n'a pas à penser à insérer des instructions pour rafraîchir le contenu de buff à chaque exécution de la boucle. Au lieu de cela, lors de l'optimisation, avec quelques hypothèses non forcées et ennuyeuses sur l'aliasing, il peut omettre ces instructions, charger buff[0] y buff[1] dans les registres du CPU une fois avant l'exécution de la boucle, et accélérer le corps de la boucle. Avant l'introduction de l'aliasing strict, le compilateur devait vivre dans un état de paranoïa en craignant que le contenu de buff pourrait être modifié par n'importe quel stockage mémoire précédent. Ainsi, pour obtenir un avantage supplémentaire en termes de performances, et en supposant que la plupart des gens ne tapent pas les pointeurs, la règle d'aliasing stricte a été introduite.

Gardez à l'esprit, si vous pensez que l'exemple est artificiel, que cela pourrait même se produire si vous passez un tampon à une autre fonction qui effectue l'envoi à votre place, si à la place vous avez.

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

Et nous avons réécrit notre boucle précédente pour tirer parti de cette fonction pratique

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

Le compilateur peut ou non être capable ou assez intelligent pour essayer de mettre en ligne SendMessage et il peut ou non décider de charger ou non le tampon à nouveau. Si SendMessage fait partie d'une autre API qui est compilée séparément, elle a probablement des instructions pour charger le contenu de Buff. Mais peut-être que vous êtes en C++ et qu'il s'agit d'une implémentation modèle d'en-tête seulement que le compilateur pense pouvoir mettre en ligne. Ou peut-être que c'est juste quelque chose que vous avez écrit dans votre fichier .c pour votre propre confort. Quoi qu'il en soit, un comportement indéfini peut toujours s'ensuivre. Même si nous savons ce qui se passe sous le capot, il s'agit toujours d'une violation de la règle et aucun comportement bien défini n'est garanti. Ainsi, le simple fait d'intégrer une fonction qui prend notre tampon délimité par des mots n'est pas forcément utile.

Alors comment puis-je contourner ce problème ?

  • Utilisez une union. La plupart des compilateurs supportent cela sans se plaindre de l'aliasing strict. Ceci est autorisé en C99 et explicitement autorisé en C11.

      union {
          Msg msg;
          unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
      };
  • Vous pouvez désactiver l'aliasing strict dans votre compilateur ( f[no-]strict-aliasing dans gcc))

  • Vous pouvez utiliser char* pour l'aliasing au lieu du mot de votre système. Les règles prévoient une exception pour char* (y compris signed char y unsigned char ). Il est toujours supposé que char* alias d'autres types. Cependant, cela ne fonctionne pas dans l'autre sens : il n'est pas supposé que votre structure aliène un tampon de caractères.

Attention aux débutants

Ce n'est là qu'un des champs de mines potentiels lorsque l'on superpose deux types. Vous devez également vous renseigner sur endiveté , alignement des mots et comment traiter les problèmes d'alignement par Structures d'emballage correctement.

Note de bas de page

1 Les types auxquels C 2011 6.5 7 permet à une lvalue d'accéder sont les suivants :

  • un type compatible avec le type effectif de l'objet,
  • une version qualifiée d'un type compatible avec le type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant au type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, de manière récursive, un membre d'un sous-agrégat ou d'une union contenue), ou
  • un type de caractère.

22 votes

Je viens après la bataille, il semble que unsigned char* être utilisé loin char* à la place ? J'ai tendance à utiliser unsigned char plutôt que char comme type sous-jacent pour byte parce que mes octets ne sont pas signés et que je ne veux pas les bizarreries du comportement des signataires (notamment en ce qui concerne le débordement).

31 votes

@Matthieu : La signature ne fait aucune différence pour les règles d'alias, donc l'utilisation de unsigned char * c'est bon.

0 votes

Pourquoi pas ? SendToStupidApi(&msg->a) ?

261voto

Niall Points 2074

La meilleure explication que j'ai trouvée est celle de Mike Acton, Comprendre l'aliénation stricte . Il est un peu axé sur le développement de la PS3, mais il s'agit essentiellement de GCC.

Extrait de l'article :

"L'aliasing strict est une hypothèse, faite par le compilateur C (ou C++), selon laquelle les pointeurs déréférencés d'objets de types différents ne feront jamais référence au même emplacement mémoire (c'est-à-dire qu'ils s'aliasent les uns les autres)."

Donc, en gros, si vous avez un int* pointant vers une mémoire contenant un int et ensuite vous pointez un float* à cette mémoire et l'utiliser comme une float vous enfreignez la règle. Si votre code ne respecte pas cette règle, alors l'optimiseur du compilateur cassera très probablement votre code.

L'exception à la règle est un char* qui peut pointer vers n'importe quel type.

8 votes

Quelle est donc la manière canonique d'utiliser légalement la même mémoire avec des variables de deux types différents ? ou tout le monde se contente-t-il de copier ?

5 votes

La page de Mike Acton est défectueuse. La partie de "Casting through a union (2)", au moins, est carrément fausse ; le code qu'il prétend être légal ne l'est pas.

20 votes

@davmac : Les auteurs de C89 n'ont jamais eu l'intention de forcer les programmeurs à sauter à travers des cerceaux. Je trouve tout à fait bizarre l'idée qu'une règle qui existe dans le seul but de l'optimisation soit interprétée de telle manière qu'elle oblige les programmeurs à écrire du code qui copie de manière redondante des données dans l'espoir qu'un optimiseur supprime le code redondant.

146voto

Ben Voigt Points 151460

Il s'agit de la règle d'aliasing stricte, que l'on trouve dans la section 3.10 de la norme C++03 norme (les autres réponses fournissent une bonne explication, mais aucune ne fournit la règle elle-même) :

Si un programme tente d'accéder à la valeur stockée d'un objet par le biais d'une lvalue autre que l'un des types suivants, le comportement est indéfini :

  • le type dynamique de l'objet,
  • une version qualifiée cv du type dynamique de l'objet,
  • un type qui est le type signé ou non signé correspondant au type dynamique de l'objet,
  • un type qui est le type signé ou non signé correspondant à une version qualifiée cv du type dynamique de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses membres (y compris, de manière récursive, un membre d'un sous-agrégat ou d'une union contenue),
  • un type qui est un type de classe de base (éventuellement qualifié de cv) du type dynamique de l'objet,
  • a char o unsigned char type.

C++11 y C++14 le libellé (les modifications sont soulignées) :

Si un programme tente d'accéder à la valeur stockée d'un objet par l'intermédiaire d'un fichier glvalue d'un autre type que l'un des types suivants, le comportement est indéfini :

  • le type dynamique de l'objet,
  • une version qualifiée cv du type dynamique de l'objet,
  • un type similaire (tel que défini au point 4.4) au type dynamique de l'objet,
  • un type qui est le type signé ou non signé correspondant au type dynamique de l'objet,
  • un type qui est le type signé ou non signé correspondant à une version qualifiée cv du type dynamique de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses ou des éléments de données non statiques (y compris, de manière récursive, un ou un élément de données non statique d'un sous-agrégat ou d'une union contenue),
  • un type qui est un type de classe de base (éventuellement qualifié de cv) du type dynamique de l'objet,
  • a char o unsigned char type.

Deux changements ont été mineurs : glvalue au lieu de lvalue et la clarification de l'affaire des agrégats et des syndicats.

La troisième modification apporte une garantie plus forte (elle assouplit la règle de l'aliasing fort) : Le nouveau concept de types similaires qui sont maintenant sûrs d'être aliasés.


De même, le C formulation (C99 ; ISO/IEC 9899:1999 6.5/7 ; la même formulation est utilisée dans ISO/IEC 9899:2011 §6.5 ¶7) :

Pour accéder à la valeur stockée d'un objet, il faut utiliser une expression lvalue qui présente l'un des types suivants 73) ou 88) :

  • un type compatible avec le type effectif de l'objet,
  • une version qualifiée d'un type compatible avec le type effectif de l'objet. l'objet,
  • un type qui est le type signé ou non signé qui correspond au type effectif de l'objet,
  • un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet. version qualifiée du type effectif de l'objet,
  • un type d'agrégat ou d'union qui comprend un des types susmentionnés parmi ses membres (y compris, de manière récursive, un membre d'un sous-agrégat ou union contenue), ou
  • un type de caractère.

73) ou 88) L'objectif de cette liste est de spécifier les circonstances dans lesquelles un objet peut ou ne peut pas être aliasé.

9 votes

Ben, comme les gens sont souvent dirigés ici, je me suis permis d'ajouter aussi la référence à la norme C, par souci d'exhaustivité.

0 votes

@Kos : C'est cool, merci ! Pouvez-vous également nous dire si le crénelage strict était exigé par la norme C89/90 ? (Je crois me souvenir que non, qu'il a été introduit en même temps que la norme C89/90. restrict mot-clé, mais je ne suis pas sûr).

1 votes

Regardez le raisonnement de C89 cs.technion.ac.il/utilisateurs/yechiel/CS/C++draft/rationale.pdf section 3.3 qui en parle.

47voto

Patrick Points 607

L'aliasing strict ne concerne pas seulement les pointeurs, il affecte aussi les références. J'ai écrit un article à ce sujet pour le wiki du développeur Boost et il a été si bien accueilli que j'en ai fait une page sur mon site de conseil. J'ai écrit un article à ce sujet pour le wiki de boost developer et il a été si bien accueilli que j'en ai fait une page sur mon site de conseil. Livre blanc sur l'aliasing strict . Il explique notamment pourquoi les unions sont un comportement risqué pour le C++, et pourquoi l'utilisation de memcpy est la seule solution portable à la fois pour le C et le C++. J'espère que cela vous sera utile.

3 votes

" L'aliasing strict ne concerne pas seulement les pointeurs, mais aussi les références. " En fait, il s'agit de lvalues . " utiliser memcpy est la seule solution portable " Écoutez !

3 votes

La section "Une autre version brisée, se référant deux fois" de l'article n'a aucun sens. Même s'il y avait un point de séquence, cela ne donnerait pas le bon résultat. Peut-être vouliez-vous utiliser des opérateurs de décalage au lieu d'affectations de décalage ? Mais alors le code est bien défini et fait la bonne chose.

5 votes

Bon papier. Mon point de vue : (1) ce "problème" d'alias est une réaction excessive à la mauvaise programmation - essayer de protéger le mauvais programmeur de ses mauvaises habitudes. Si le programmeur a de bonnes habitudes, alors cet aliasing n'est qu'une nuisance et les vérifications peuvent être désactivées en toute sécurité. (2) L'optimisation côté compilateur ne devrait être effectuée que dans des cas bien connus et devrait, en cas de doute, suivre strictement le code source ; forcer le programmeur à écrire du code pour répondre aux idiosyncrasies du compilateur est, tout simplement, une erreur. Il est encore pire d'en faire un élément de la norme.

36voto

Ingo Blackman Points 433

En complément à ce que Doug T. a déjà écrit, voici est un cas de test simple qui le déclenche probablement avec gcc :

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

Compiler avec gcc -O2 -o check check.c . Habituellement (avec la plupart des versions de gcc que j'ai essayées), cela produit un "problème d'aliasing strict", parce que le compilateur suppose que "h" ne peut pas être la même adresse que "k" dans la fonction "check". Pour cette raison, le compilateur optimise la fonction if (*h == 5) et appelle toujours le printf.

Pour ceux qui sont intéressés, voici le code assembleur x64, produit par gcc 4.6.3, fonctionnant sur ubuntu 12.04.2 pour x64 :

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

Ainsi, la condition if a complètement disparu du code assembleur.

0 votes

Si vous ajoutez un deuxième court * j à check() et que vous l'utilisez ( *j = 7 ) alors l'optimisation disparaît puisque ggc ne fait pas si h et j ne pointent pas vers la même valeur. oui l'optimisation est vraiment intelligente.

2 votes

Pour rendre les choses plus amusantes, utilisez des pointeurs vers des types qui ne sont pas compatibles, mais qui ont la même taille et la même représentation (sur certains systèmes, c'est le cas par exemple de long long* y int64_t *). On pourrait s'attendre à ce qu'un compilateur sain reconnaisse qu'un fichier long long* y int64_t* pourraient accéder au même stockage s'ils sont stockés de manière identique, mais ce traitement n'est plus à la mode.

2 votes

Grr... x64 est une convention Microsoft. Utilisez amd64 ou x86_64 à la place.

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