Question Quelle est la règle stricte d'aliasing?


En demandant à propos de comportement commun indéfini en C, des âmes plus éclairées que je ne parlais de la stricte règle des alias.
De quoi parlent-ils?


668
2017-09-19 01:30


origine


Réponses:


Une situation typique où vous rencontrez des problèmes d'alias stricts est lors de la superposition d'une structure (comme un message de périphérique / réseau) sur un tampon de la taille du mot de votre système (comme un pointeur sur uint32_ts ou uint16_ts). Lorsque vous superposez une structure sur un tel tampon, ou un tampon sur une telle structure par l'intermédiaire d'un cast de pointeur, vous pouvez facilement violer les règles strictes d'alias.

Donc, dans ce type d'installation, si je veux envoyer un message à quelque chose, je devrais avoir deux pointeurs incompatibles pointant vers le même bloc 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 d'alias strict rend cette configuration illégale: déréférencement d'un pointeur qui alias un objet qui n'est pas type compatible ou l'un des autres types autorisés par C 2011 6.5 paragraphe 71 est un comportement indéfini. Malheureusement, vous pouvez toujours coder de cette façon, peut être obtenir quelques avertissements, compilez-le bien, seulement pour avoir un comportement inattendu bizarre quand 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 n'est pas défini, nous devons réfléchir à ce que la règle d'alias strict achète le compilateur. Fondamentalement, avec cette règle, il n'a pas à penser à insérer des instructions pour rafraîchir le contenu de buff chaque course de la boucle. Au lieu de cela, lors de l'optimisation, avec des hypothèses peu encourageantes sur l'aliasing, il peut omettre ces instructions, charger buff[0] et buff[1] dans les registres 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 que le contenu de buff pourrait changer à tout moment de n'importe où par n'importe qui. Donc, pour obtenir un avantage supplémentaire en termes de performances, et en supposant que la plupart des gens n'utilisent pas de pointeurs de type, la règle d'alias strict a été introduite.

Gardez à l'esprit, si vous pensez que l'exemple est artificiel, cela peut même arriver si vous passez un tampon à une autre fonction en faisant l'envoi pour vous, si à la place vous avez.

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

Et réécrit notre boucle précédente pour profiter 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 ne peut pas être capable ou assez intelligent pour essayer d'intégrer SendMessage et il peut ou non décider de charger ou de ne pas charger à nouveau le buff. Si SendMessage fait partie d'une autre API compilée séparément, elle a probablement des instructions pour charger le contenu du buff. Encore une fois, peut-être vous êtes en C ++ et ceci est une implémentation d'en-tête basée sur un modèle que le compilateur pense pouvoir intégrer. Ou peut-être que c'est juste quelque chose que vous avez écrit dans votre fichier .c pour votre propre commodité. Quoi qu'il en soit, un comportement indéfini peut encore s'ensuivre. Même lorsque nous savons ce qui se passe sous le capot, c'est toujours une violation de la règle, donc aucun comportement bien défini n'est garanti. Donc, simplement en enveloppant une fonction qui prend notre mot tampon délimité ne contribue pas nécessairement.

Alors, comment puis-je contourner cela?

  • Utilisez un syndicat. La plupart des compilateurs le supportent 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'alias strict dans votre compilateur (f [no-] strict-aliasing en gcc))

  • Vous pouvez utiliser char* pour aliasing au lieu de la parole de votre système. Les règles permettent une exception pour char* (comprenant signed char et unsigned char). Il est toujours supposé que char* alias d'autres types. Cependant cela ne fonctionnera pas dans l'autre sens: il n'y a aucune supposition que votre structure aliases un tampon de chars.

Débutant méfiez-vous

C'est seulement un champ de mines potentiel en superposant deux types sur l'un l'autre. Vous devriez aussi apprendre à propos de endianness, alignement des mots, et comment gérer les problèmes d'alignement à travers structures d'emballage correctement.

note de bas de page

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

  • 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 agrégat ou union qui comprend l'un des types susmentionnés parmi ses membres (y compris, récursivement, un membre d'une union subagrégée ou confinée), ou
  • un type de caractère.

479
2017-09-19 01:38



La meilleure explication que j'ai trouvée est de Mike Acton, Comprendre l'alias strict. Il se concentre un peu sur le développement de la PS3, mais il ne s'agit que de GCC.

De l'article:

«L'aliasing strict est une supposition, faite par le compilateur C (ou C ++), que les pointeurs de déréférencement vers des objets de types différents ne se réfèrent jamais au même emplacement de mémoire (c'est-à-dire alias les uns les autres).

Donc, fondamentalement, si vous avez un int* pointant vers un peu de mémoire contenant un int et puis vous pointez un float* à cette mémoire et l'utiliser comme float vous cassez la règle. Si votre code ne respecte pas cela, alors l'optimiseur du compilateur va probablement casser votre code.

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


210
2017-08-10 04:43



C'est la règle stricte d'aliasing, trouvée dans la section 3.10 du C ++ 03 standard (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 via une valeur lvalue d'un des types suivants, le comportement n'est pas défini:

  • le type dynamique de l'objet,
  • une version qualifiée de 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 cv-qualifiée 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, récursivement, un membre d'une union subagrégée ou confinée),
  • un type qui est un type de classe de base (éventuellement qualifié de cv) du type dynamique de l'objet,
  • une char ou unsigned char type.

C ++ 11 et C ++ 14 libellé (changements mis en évidence):

Si un programme tente d'accéder à la valeur stockée d'un objet via un Glvalue de l'un des types suivants, le comportement n'est pas défini:

  • le type dynamique de l'objet,
  • une version qualifiée de cv du type dynamique de l'objet,
  • un type similaire (tel que défini en 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 cv-qualifiée du type dynamique de l'objet,
  • un type d'agrégat ou d'union qui inclut l'un des types susmentionnés parmi ses éléments ou membres de données non statiques (y compris, récursivement, un élément ou membre de données non statique d'une union subagrégée ou confinée),
  • un type qui est un type de classe de base (éventuellement qualifié de cv) du type dynamique de l'objet,
  • une char ou unsigned char type.

Deux changements étaient petits: Glvalue au lieu de lvalueet clarification de l'affaire agrégée / syndicale.

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


Également C libellé (C99, ISO / CEI 9899: 1999 6.5 / 7, le même libellé est utilisé dans l'ISO / CEI 9899: 2011 §6.5 ¶7):

Un objet doit avoir sa valeur stockée accessible uniquement par un lvalue   expression qui a l'un des types suivants  73) ou 88):

  • un type compatible avec le type effectif de l'objet,
  • une version quali fi ée d'un type compatible avec le type effectif de   L'object,
  • 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 correspondant à un   version quali fi ée du type effectif de l'objet,
  • un agrégat ou un type d'union qui comprend l'un des   types parmi ses membres (y compris, récursivement, un membre d'un   union subagrégée ou contenue), ou
  • un type de caractère.

 73) ou 88) Le but de cette liste est de spécifier les circonstances dans lesquelles un objet peut ou non être un alias.


124
2018-06-19 23:46



L'aliasing strict ne se réfère pas seulement aux 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 reçu que je l'ai transformé en page sur mon site de consulting. Il explique complètement ce que c'est, pourquoi il dérange tant les gens et que faire à ce sujet. Livre blanc sur l'alias strict. En particulier, il explique pourquoi les syndicats sont un comportement risqué pour C ++, et pourquoi l'utilisation de memcpy est le seul correctif portable à la fois sur C et C ++. J'espère que c'est utile.


39
2018-05-14 02:37



Comme addendum à ce que Doug T. a déjà écrit, ici 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) ceci produit le "problème strict d'aliasing", parce que le compilateur suppose que "h" ne peut pas être la même adresse que "k" dans la fonction "vérifier". Pour cette raison, le compilateur optimise if (*h == 5) loin et appelle toujours le printf.

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

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

Donc, la condition if est complètement supprimée du code assembleur.


30
2017-09-19 01:38



Type de punition via des jets de pointeurs (par opposition à l'utilisation d'une union) est un exemple majeur de rupture de l'aliasing strict.


15
2018-04-26 22:42



Selon la logique C89, les auteurs de la norme ne voulaient pas exiger que les compilateurs reçoivent un code comme:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

devrait être nécessaire pour recharger la valeur de x entre la cession et la déclaration de retour afin de permettre la possibilité que p pourrait pointer vers xet l'assignation à *p pourrait par conséquent modifier la valeur de x. La notion qu'un compilateur devrait avoir le droit de supposer qu'il n'y aura pas d'alias dans des situations comme ci-dessus était non controversé.

Malheureusement, les auteurs de la C89 ont écrit leur règle d'une manière qui, si elle était lue littéralement, rendrait même la fonction suivante invoquant un comportement indéterminé:

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

car il utilise une lvalue de type int pour accéder à un objet de type struct S, et int n'est pas parmi les types qui peuvent être utilisés pour accéder à un struct S. Parce qu'il serait absurde de traiter toute utilisation de membres de structures et de syndicats de type non-caractère comme comportement indéfini, presque tout le monde reconnaît qu'il y a au moins quelques circonstances où une valeur d'un type peut être utilisée pour accéder à un objet d'un autre type . Malheureusement, le Comité des normes C n'a pas défini quelles étaient ces circonstances.

Une grande partie du problème est le résultat du rapport de défaut # 028, qui a demandé au sujet du comportement d'un programme comme:

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

Le rapport de défaut # 28 indique que le programme invoque un comportement indéfini parce que l'action d'écrire un membre d'union de type "double" et en lisant un de type "int" invoque le comportement défini par l'implémentation. Un tel raisonnement est absurde, mais constitue la base pour les règles de type efficace qui compliquent inutilement la langue tout en ne faisant rien pour résoudre le problème original.

La meilleure façon de résoudre le problème d'origine serait probablement de traiter note sur le but de la règle comme si elle était normative, et fait la règle est inapplicable sauf dans les cas impliquant des accès conflictuels utilisant des alias. Donné quelque chose comme:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

Il n'y a pas de conflit au sein de inc_int parce que tous les accès au stockage accessible par *p sont faites avec une lvalue de type intet il n'y a pas de conflit test car p est visiblement dérivé d'un struct Set d'ici la prochaine fois s est utilisé, tous les accès à ce stockage qui sera jamais fait par p aura déjà eu lieu.

Si le code a été légèrement modifié ...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

Ici, il y a un conflit d'alias entre p et l'accès à s.x sur la ligne marquée car à ce moment de l'exécution une autre référence existe qui sera utilisé pour accéder au même stockage.

Si le rapport de défauts 028 indiquait que l'exemple original invoquait UB en raison du chevauchement entre la création et l'utilisation des deux pointeurs, cela aurait rendu les choses beaucoup plus claires sans avoir à ajouter des «types effectifs» ou une autre complexité de ce type.


10
2017-12-24 12:04



Après avoir lu beaucoup de réponses, je ressens le besoin d'ajouter quelque chose:

Aliasing strict (que je décrirai dans un peu) est important parce que:

  1. L'accès à la mémoire peut être coûteux (performance sage), ce qui explique pourquoi les données sont manipulées dans les registres de la CPU avant d'être réécrit à la mémoire physique.

  2. Si les données de deux registres de processeurs différents sont écrites dans le même espace mémoire, nous ne pouvons pas prédire quelles données vont "survivre" lorsque nous codons en C.

    Dans l'assemblage, où nous codons manuellement le chargement et le déchargement des registres du CPU, nous saurons quelles données restent intactes. Mais C (heureusement) résume ce détail.

Étant donné que deux pointeurs peuvent pointer vers le même emplacement dans la mémoire, cela peut entraîner code complexe qui gère les collisions possibles.

Ce code supplémentaire est lent et fait mal la performance car il effectue des opérations de lecture / écriture en mémoire supplémentaires qui sont à la fois plus lentes et (éventuellement) inutiles.

le La règle stricte d'aliasing nous permet d'éviter le code machine redondant dans les cas où il devrait être sans risque de supposer que deux pointeurs ne pointent pas sur le même bloc de mémoire (voir aussi restrict mot-clé).

Le Strict aliasing indique qu'il est prudent de supposer que les pointeurs vers différents types pointent vers des emplacements différents dans la mémoire.

Si un compilateur remarque que deux pointeurs pointent vers des types différents (par exemple, un int * et un float *), il suppose que l'adresse mémoire est différente et ne sera pas protéger contre les collisions d'adresses de mémoire, résultant en un code machine plus rapide.

Par exemple:

Supposons la fonction suivante:

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

Afin de gérer le cas dans lequel a == b (les deux pointeurs pointent vers la même mémoire), nous devons ordonner et tester la façon dont nous chargeons les données de la mémoire vers les registres de la CPU, de sorte que le code peut se terminer comme ceci:

  1. charge a et b de mémoire.

  2. ajouter a à b.

  3. enregistrer  b et recharger  a.

    (sauvegarder du registre CPU en mémoire et charger de la mémoire dans le registre CPU).

  4. ajouter b à a.

  5. enregistrer a (du registre CPU) à la mémoire.

L'étape 3 est très lente car elle doit accéder à la mémoire physique. Cependant, il est nécessaire de protéger contre les cas où a et b pointez sur la même adresse mémoire.

Un alias strict nous permettrait d'éviter cela en disant au compilateur que ces adresses mémoire sont nettement différentes (ce qui, dans ce cas, permettra encore une optimisation supplémentaire qui ne peut pas être effectuée si les pointeurs partagent une adresse mémoire).

  1. Cela peut être dit au compilateur de deux façons, en utilisant différents types de pointer vers. c'est à dire.:

    void merge_two_numbers(int *a, long *b) {...}
    
  2. En utilisant le restrict mot-clé. c'est à dire.:

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

Maintenant, en satisfaisant à la règle Strict Aliasing, l'étape 3 peut être évitée et le code fonctionnera beaucoup plus rapidement.

En fait, en ajoutant restrict mot-clé, toute la fonction pourrait être optimisée pour:

  1. charge a et b de mémoire.

  2. ajouter a à b.

  3. enregistrer le résultat à la fois aet à b.

Cette optimisation n'aurait pas pu être faite auparavant, en raison de la collision possible (où a et b serait triplé au lieu de doublé).


8
2017-07-08 02:07