Quelle est la règle stricte d'alias?

805

Lorsqu'on pose des questions sur un comportement non défini commun en C , les gens se réfèrent parfois à la règle stricte d'alias.
De quoi parlent-ils?

Benoit
la source
12
@Ben Voigt: les règles d'alias sont différentes pour c ++ et c. Pourquoi cette question est-elle étiquetée avec cet c++faq.
MikeMB du
6
@MikeMB: Si vous consultez l'historique, vous verrez que j'ai conservé les balises telles qu'elles étaient à l'origine, malgré la tentative de certains autres experts de changer la question des réponses existantes. En outre, la dépendance de la langue et de la version est une partie très importante de la réponse à "Quelle est la règle stricte d'alias?" et connaître les différences est important pour les équipes qui migrent du code entre C et C ++, ou écrivent des macros à utiliser dans les deux.
Ben Voigt
6
@Ben Voigt: En fait - pour autant que je sache - la plupart des réponses ne concernent que c et non c ++. ). Pour la plupart, les règles et l'idée générale sont les mêmes bien sûr, mais surtout, lorsque les syndicats sont concernés, les réponses ne s'appliquent pas au c ++. Je suis un peu inquiet, que certains programmeurs c ++ recherchent la règle stricte d'alias et supposent simplement que tout ce qui est indiqué ici s'applique également à c ++.
MikeMB
D'un autre côté, je conviens qu'il est problématique de changer la question après que beaucoup de bonnes réponses ont été publiées et que le problème est mineur de toute façon.
MikeMB
1
@MikeMB: Je pense que vous verrez que l'accent C sur la réponse acceptée, ce qui la rend incorrecte pour C ++, a été modifié par un tiers. Cette partie devrait probablement être révisée à nouveau.
Ben Voigt

Réponses:

562

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

Donc, dans ce type de configuration, 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 naïvement coder quelque chose comme ça (sur un système avec sizeof(int) == 2):

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 stricte rend cette configuration illégale: déréférencer un pointeur qui alias un objet qui n'est pas d'un 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 des avertissements, le faire compiler correctement, seulement pour avoir un comportement inattendu étrange lorsque vous exécutez le code.

(GCC semble quelque peu incohérent dans sa capacité à donner des avertissements de pseudonyme, 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'aliasing stricte achète le compilateur. Fondamentalement, avec cette règle, il n'a pas à penser à insérer des instructions pour actualiser le contenu de buffchaque exécution de la boucle. Au lieu de cela, lors de l'optimisation, avec certaines hypothèses ennuyeusement non appliquées sur l'alias, il peut omettre ces instructions, charger buff[0]et buff[1] dans les registres du processeur une fois avant l'exécution de la boucle et accélérer le corps de la boucle. Avant l'introduction d'un aliasing strict, le compilateur devait vivre dans un état de paranoïa dont le contenu buffpouvait changer à tout moment et 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 ne tapent pas de pointeurs de pun, la règle d'aliasing stricte 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 effectuant l'envoi pour vous, si vous l'avez à la place.

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 non être en mesure ou suffisamment intelligent pour essayer d'inclure SendMessage et il peut ou non décider de charger ou de ne pas charger à nouveau buff. Si SendMessagefait partie d'une autre API compilée séparément, elle contient probablement des instructions pour charger le contenu de buff. Là encore, vous êtes peut-être en C ++ et il s'agit d'une implémentation basée uniquement sur un modèle que le compilateur pense pouvoir incorporer. Ou peut-être que c'est juste quelque chose que vous avez écrit dans votre fichier .c pour votre propre convenance. Quoi qu'il en soit, un comportement indéfini pourrait encore se produire. 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 encapsulant une fonction qui prend notre tampon délimité par des mots n'aide pas nécessairement.

Alors, comment puis-je contourner cela?

  • Utilisez un syndicat. La plupart des compilateurs prennent en charge cela sans se plaindre d'un alias 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 dans gcc))

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

Attention aux débutants

Ce n'est qu'un champ de mines potentiel lors de la superposition de deux types l'un sur l'autre. Vous devriez également vous renseigner sur l' endianité , l' alignement des mots et la façon de traiter les problèmes d'alignement via correctement les des structures d'emballage .

note de bas de page

1 Les types auxquels C 2011 6.5 7 permet à une valeur 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 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 sous-agrégée ou contenue), ou
  • un type de caractère.
Doug T.
la source
17
Je viens après la bataille, il semble .. peut- unsigned char*être être utilisé à la char*place? J'ai tendance à utiliser unsigned charplutôt que charcomme type sous-jacent bytecar mes octets ne sont pas signés et je ne veux pas l'étrangeté du comportement signé (notamment wrt to overflow)
Matthieu M.
30
@Matthieu: la signature ne fait aucune différence pour les règles d'alias, donc l'utilisation unsigned char *est acceptable.
Thomas Eding
22
N'est-ce pas un comportement indéfini de lire à partir d'un membre du syndicat différent du dernier à qui on a écrit?
R. Martinho Fernandes
23
Bollocks, cette réponse est complètement à l'envers . L'exemple qu'il montre comme illégal est en fait légal, et l'exemple qu'il montre comme légal est en fait illégal.
R. Martinho Fernandes
7
Votre uint32_t* buff = malloc(sizeof(Msg));et les unsigned int asBuffer[sizeof(Msg)];déclarations de tampon d' union suivantes auront des tailles différentes et aucune n'est correcte. L' mallocappel repose sur l'alignement de 4 octets sous le capot (ne le faites pas) et l'union sera 4 fois plus grande qu'elle ne devrait l'être ... Je comprends que c'est pour plus de clarté mais cela me dérange quand même - moins ...
nonsensickle
233

La meilleure explication que j'ai trouvée est de Mike Acton, Understanding Strict Aliasing . Il se concentre un peu sur le développement de la PS3, mais c'est essentiellement GCC.

De l'article:

"Un aliasing strict est une hypothèse, faite par le compilateur C (ou C ++), que le déréférencement de pointeurs vers des objets de différents types ne fera jamais référence au même emplacement de mémoire (c'est-à-dire un alias entre eux.)"

Donc, fondamentalement, si vous avez un int*pointage vers une mémoire contenant un int, puis que vous pointez un float*vers cette mémoire et que floatvous l' utilisez comme vous enfreignez la règle. Si votre code ne respecte pas cela, alors l'optimiseur du compilateur cassera très probablement votre code.

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

Niall
la source
6
Alors, quelle est la manière canonique d'utiliser légalement la même mémoire avec des variables de 2 types différents? ou tout le monde copie-t-il simplement?
jiggunjer
4
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.
davmac
11
@davmac: Les auteurs de C89 n'ont jamais voulu qu'il oblige les programmeurs à sauter à travers les cerceaux. Je trouve tout à fait bizarre l'idée qu'une règle qui existe dans le seul but d'optimisation devrait être interprétée de manière à obliger les programmeurs à écrire du code qui copie de manière redondante les données dans l'espoir qu'un optimiseur supprime le code redondant.
supercat
1
@curiousguy: "Impossible d'avoir des syndicats"? Premièrement, l'objectif initial / principal des syndicats n'est aucunement lié à l'aliasing. Deuxièmement, la spécification du langage moderne autorise explicitement l'utilisation des unions pour le crénelage. Le compilateur doit remarquer qu'une union est utilisée et traiter la situation est une manière spéciale.
2017
5
@curiousguy: Faux. Premièrement, l'idée conceptuelle originale derrière les unions était qu'à tout moment il n'y a qu'un seul objet membre "actif" dans l'objet union donné, tandis que les autres n'existent tout simplement pas. Il n'y a donc pas "d'objets différents à la même adresse" comme vous semblez le croire. Deuxièmement, les violations d'alias dont tout le monde parle consistent à accéder à un objet en tant qu'objet différent, pas simplement à avoir deux objets avec la même adresse. Tant qu'il n'y a pas d' accès de type punning , il n'y a pas de problème. C'était l'idée originale. Plus tard, la punition par le biais des syndicats a été autorisée.
2017
133

Il s'agit de la règle d'alias stricte, trouvée dans la section 3.10 de la norme C ++ 03 (d'autres réponses fournissent une bonne explication, mais aucune n'a fourni la règle elle-même):

Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur l autre que l'un des types suivants, le comportement n'est pas dé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, récursivement, un membre d'une union sous-agrégée ou contenue),
  • un type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
  • a charou unsigned chartapez.

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

Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur gl autre que l'un des types suivants, le comportement n'est pas dé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 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 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 éléments ou membres de données non statiques (y compris, récursivement, un élément ou un membre de données non statiques d'une union sous-agrégée ou contenue),
  • un type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,
  • a charou unsigned chartapez.

Deux changements étaient mineurs : glvalue au lieu de lvalue et clarification du cas d'agrégation / d'union.

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


Aussi le libellé C (C99; ISO / IEC 9899: 1999 6.5 / 7; le même libellé exact est utilisé dans ISO / IEC 9899: 2011 §6.5 §7):

Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue 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'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 quali fi é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, récursivement, un membre d'une union sous-agré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 replié.

Ben Voigt
la source
7
Ben, comme les gens sont souvent dirigés ici, je me suis permis d'ajouter la référence à la norme C aussi, par souci d'exhaustivité.
Kos
1
Consultez la section 3.3 de la justification C89 cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdf qui en parle.
phorgan1
2
Si l'on a une valeur l d'un type de structure, prend l'adresse d'un membre et la transmet à une fonction qui l'utilise comme pointeur vers le type de membre, cela serait-il considéré comme accédant à un objet du type de membre (légal), ou un objet de type structure (interdit)? Un grand nombre de code suppose qu'il est des structures juridiques à l'accès à de telle façon, et je pense que beaucoup de gens à brailler une règle qui a été considérée comme interdisant de telles actions, mais on ne sait pas quelles sont les règles exactes. De plus, les syndicats et les structures sont traités de la même manière, mais les règles raisonnables pour chacun devraient être différentes.
supercat
2
@supercat: La façon dont la règle pour les structures est formulée, l'accès réel est toujours au type primitif. Ensuite, l'accès via une référence au type primitif est légal car les types correspondent, et l'accès via une référence au type de structure conteneur est légal car il est spécialement autorisé.
Ben Voigt
2
@BenVoigt: Je ne pense pas que la séquence initiale commune fonctionne, sauf si les accès se font via l'union. Voir goo.gl/HGOyoK pour voir ce que fait gcc. Si l'accès à une lvalue de type union via une lvalue d'un type de membre (n'utilisant pas d'opérateur union-member-access) était légal, alors il wow(&u->s1,&u->s2)devrait être légal même lorsqu'un pointeur est utilisé pour modifier u, et cela annulerait la plupart des optimisations que le règle d'aliasing a été conçue pour faciliter.
supercat
82

Remarque

Ceci est extrait de mon "Qu'est-ce que la règle de repliement strict et pourquoi nous en soucions-nous?"rédiger.

Qu'est-ce qu'un aliasing strict?

En C et C ++, l'aliasing a à voir avec quels types d'expression nous sommes autorisés à accéder aux valeurs stockées. En C et C ++, la norme spécifie quels types d'expression sont autorisés à alias quels types. Le compilateur et l'optimiseur sont autorisés à supposer que nous suivons strictement les règles d'alias, d'où le terme de règle d'alias stricte . Si nous tentons d'accéder à une valeur en utilisant un type non autorisé, elle est classée comme comportement non défini ( UB ). Une fois que nous avons un comportement indéfini, tous les paris sont désactivés, les résultats de notre programme ne sont plus fiables.

Malheureusement, avec des violations d'alias strictes, nous obtiendrons souvent les résultats escomptés, laissant la possibilité qu'une future version d'un compilateur avec une nouvelle optimisation casse le code que nous pensions être valide. Ce n'est pas souhaitable et c'est un objectif valable de comprendre les règles strictes d'alias et comment éviter de les violer.

Pour mieux comprendre pourquoi nous nous soucions, nous discuterons des problèmes qui surviennent lors de la violation de règles d'aliasing strictes, de la punition de type car les techniques courantes utilisées dans la punition de type violent souvent les règles d'aliasing strictes et comment taper correctement pun.

Exemples préliminaires

Regardons quelques exemples, puis nous pourrons parler exactement de ce que disent les normes, examiner d'autres exemples et voir comment éviter l'aliasing strict et les violations de capture que nous avons manquées. Voici un exemple qui ne devrait pas surprendre ( exemple en direct ):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

Nous avons un int * pointant vers la mémoire occupée par un int et ceci est un aliasing valide. L'optimiseur doit supposer que les affectations via ip pourraient mettre à jour la valeur occupée par x .

L'exemple suivant montre un alias qui conduit à un comportement indéfini ( exemple en direct ):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

Dans la fonction foo, nous prenons un int * et un float * , dans cet exemple, nous appelons foo et définissons les deux paramètres pour pointer vers le même emplacement mémoire qui dans cet exemple contient un int . Remarque, le reinterpret_cast indique au compilateur de traiter l'expression comme si elle avait le type spécifié par son paramètre de modèle. Dans ce cas, nous lui disons de traiter l'expression & x comme si elle avait le type float * . Nous pouvons naïvement nous attendre à ce que le résultat du deuxième cout soit 0, mais avec l'optimisation activée en utilisant -O2, gcc et clang produisent le résultat suivant:

0
1

Ce qui n'est peut-être pas prévu, mais est parfaitement valide car nous avons invoqué un comportement indéfini. Un flottant ne peut pas alias valablement un objet int . Par conséquent, l'optimiseur peut supposer que la constante 1 stockée lorsque le déréférencement i sera la valeur de retour puisqu'un magasin via f n'a pas pu affecter valablement un objet int . Brancher le code dans l'Explorateur de compilateur montre que c'est exactement ce qui se passe ( exemple en direct ):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

L'optimiseur utilisant l' analyse d'alias basée sur le type (TBAA) suppose que 1 sera renvoyé et déplace directement la valeur constante dans le registre eax qui contient la valeur de retour. TBAA utilise les règles de langues sur les types autorisés à alias pour optimiser les chargements et les magasins. Dans ce cas, TBAA sait qu'un float ne peut pas alias et int et optimise la charge de i .

Maintenant, au livre de règles

Que dit exactement la norme que nous sommes autorisés et non autorisés à le faire? Le langage standard n'est pas simple, donc pour chaque élément, je vais essayer de fournir des exemples de code qui en démontrent le sens.

Que dit la norme C11?

La norme C11 dit ce qui suit dans la section 6.5 Expressions, paragraphe 7 :

Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types suivants: 88) - un type compatible avec le type effectif de l'objet,

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

- une version qualifiée d'un type compatible avec le type effectif de l'objet,

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

- un type qui est le type signé ou non signé correspondant au type effectif de l'objet,

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

gcc / clang a une extension et qui permet également d' assigner int * non signé à int * même s'il ne s'agit pas de types compatibles.

- un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l'objet,

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

- 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 sous-agrégée ou contenue), ou

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

- un type de caractère.

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

Ce que dit le projet de norme C ++ 17

Le projet de norme C ++ 17 dans la section [basic.lval] paragraphe 11 dit:

Si un programme tente d'accéder à la valeur stockée d'un objet via une valeur gl autre que l'un des types suivants, le comportement n'est pas défini: 63 (11.1) - le type dynamique de l'objet,

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2) - une version qualifiée cv du type dynamique de l'objet,

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3) - un type similaire (tel que défini en 7.5) au type dynamique de l'objet,

(11.4) - un type qui est le type signé ou non signé correspondant au type dynamique de l'objet,

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5) - un type qui est le type signé ou non signé correspondant à une version qualifiée cv du type dynamique de l'objet,

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6) - 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 un membre de données non statique d'une union sous-agrégée ou contenue),

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7) - un type qui est un type de classe de base (éventuellement qualifié cv) du type dynamique de l'objet,

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8) - un type char, unsigned char ou std :: byte.

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

Il convient de noter que le caractère signé n'est pas inclus dans la liste ci-dessus, c'est une différence notable par rapport à C qui dit un type de caractère .

Qu'est-ce que le type Punning

Nous sommes arrivés à ce point et nous nous demandons peut-être pourquoi voudrions-nous nous alias? La réponse est généralement de taper calembour , souvent les méthodes utilisées violent les règles strictes d'alias.

Parfois, nous voulons contourner le système de types et interpréter un objet comme un type différent. C'est ce qu'on appelle le type punning , pour réinterpréter un segment de mémoire comme un autre type. Le repérage de type est utile pour les tâches qui souhaitent accéder à la représentation sous-jacente d'un objet à visualiser, transporter ou manipuler. Les domaines typiques que nous trouvons le type punning utilisé sont les compilateurs, la sérialisation, le code réseau, etc.

Traditionnellement, cela a été accompli en prenant l'adresse de l'objet, en le convertissant en un pointeur du type auquel nous voulons le réinterpréter, puis en accédant à la valeur, ou en d'autres termes par un aliasing. Par exemple:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

Comme nous l'avons vu précédemment, ce n'est pas un aliasing valide, nous invoquons donc un comportement non défini. Mais traditionnellement, les compilateurs ne profitaient pas de règles d'alias strictes et ce type de code fonctionnait généralement bien, les développeurs se sont malheureusement habitués à faire les choses de cette façon. Une méthode alternative courante pour la punition de type consiste à utiliser les unions, ce qui est valide en C mais un comportement non défini en C ++ ( voir l'exemple en direct ):

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

Cela n'est pas valide en C ++ et certains considèrent que le but des unions est uniquement d'implémenter des types de variantes et estiment que l'utilisation des unions pour le punning de type est un abus.

Comment pouvons-nous taper Pun correctement?

La méthode standard pour la punition de type en C et C ++ est memcpy . Cela peut sembler un peu lourd, mais l'optimiseur doit reconnaître l'utilisation de memcpy pour le type punning et l'optimiser et générer un registre pour enregistrer le mouvement. Par exemple, si nous savons que int64_t a la même taille que double :

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

nous pouvons utiliser memcpy :

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

À un niveau d'optimisation suffisant, tout compilateur moderne décent génère un code identique à la méthode reinterpret_cast ou la méthode d' union mentionnée précédemment pour le découpage de type . En examinant le code généré, nous voyons qu'il utilise simplement register mov ( exemple de l'explorateur de compilation en direct ).

C ++ 20 et bit_cast

En C ++ 20, nous pouvons gagner bit_cast ( implémentation disponible dans le lien de la proposition ) qui donne un moyen simple et sûr de taper-pun ainsi que d'être utilisable dans un contexte constexpr.

Ce qui suit est un exemple d'utilisation de bit_cast pour taper pun un entier non signé à flotter ( voir en direct ):

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

Dans le cas où les types To et From n'ont pas la même taille, il nous faut utiliser une structure intermédiaire15. Nous utiliserons une structure contenant un tableau de caractères sizeof (int non signé) ( suppose un entier non signé de 4 octets ) comme étant le type From et un entier non signé comme type To :

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

Il est regrettable que nous ayons besoin de ce type intermédiaire mais c'est la contrainte actuelle de bit_cast .

Détecter les violations d'alias strictes

Nous n'avons pas beaucoup de bons outils pour intercepter l'aliasing strict en C ++, les outils dont nous disposons vont détecter certains cas de violations d'aliasing strictes et certains cas de chargements et de magasins mal alignés.

gcc utilisant l'indicateur -fstrict-aliasing et -Wstrict-aliasing peut intercepter certains cas, mais pas sans faux positifs / négatifs. Par exemple, les cas suivants généreront un avertissement dans gcc ( voir en direct ):

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

bien qu'il n'attrapera pas ce cas supplémentaire ( voir en direct ):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

Bien que clang autorise ces indicateurs, il n'applique apparemment pas les avertissements.

Un autre outil dont nous disposons est ASan qui peut capturer des charges et des magasins mal alignés. Bien qu'il ne s'agisse pas directement de violations d'alias strictes, elles sont le résultat commun de violations d'alias strictes. Par exemple, les cas suivants génèrent des erreurs d'exécution lorsqu'ils sont créés avec clang en utilisant -fsanitize = adresse

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

Le dernier outil que je recommanderai est spécifique au C ++ et pas strictement un outil mais une pratique de codage, n'autorisez pas les conversions de style C. Gcc et clang produiront un diagnostic pour les modèles de style C à l'aide de -Wold-style-cast . Cela forcera tous les jeux de mots de type non défini à utiliser reinterpret_cast, en général, reinterpret_cast devrait être un indicateur pour une révision plus approfondie du code. Il est également plus facile de rechercher dans votre base de code reinterpret_cast pour effectuer un audit.

Pour C, nous avons tous les outils déjà couverts et nous avons également tis-interpreter, un analyseur statique qui analyse de manière exhaustive un programme pour un grand sous-ensemble du langage C. Étant donné un C verions de l'exemple précédent où l'utilisation de -fstrict-aliasing manque un cas ( voir en direct )

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeter est capable d'attraper les trois, l'exemple suivant appelle tis-kernal comme tis-interpreter (la sortie est éditée pour plus de concision):

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

Enfin, il y a TySan qui est actuellement en développement. Cet assainisseur ajoute des informations de vérification de type dans un segment de mémoire fantôme et vérifie les accès pour voir s'ils violent les règles d'alias. L'outil devrait potentiellement être en mesure de détecter toutes les violations d'alias, mais il peut avoir un gros temps d'exécution.

Shafik Yaghmour
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Bhargav Rao
3
Si je pouvais, +10, bien écrit et expliqué, également des deux côtés, les auteurs et programmeurs de compilateurs ... la seule critique: ce serait bien d'avoir des contre-exemples ci-dessus, pour voir ce qui est interdit par la norme, ce n'est pas évident genre de :-)
Gabriel
2
Très bonne réponse. Je regrette seulement que les exemples initiaux soient donnés en C ++, ce qui rend difficile à suivre pour des gens comme moi qui ne connaissent ou ne se soucient que du C et n'ont aucune idée de ce qui reinterpret_castpourrait faire ou de ce qui coutpourrait signifier. (Il est bon de mentionner C ++ mais la question initiale portait sur C et IIUC, ces exemples pourraient tout aussi bien être écrits en C.)
Gro-Tsen
En ce qui concerne la punition de type: donc si j'écris un tableau d'un certain type X dans un fichier, puis que je lis dans ce fichier ce tableau dans la mémoire pointé avec void *, puis je jette ce pointeur sur le vrai type de données afin de l'utiliser - c'est comportement indéfini?
Michael IV
44

L'aliasing strict ne fait pas uniquement référence aux pointeurs, il affecte également 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 une page sur mon site Web de consultation. Il explique complètement ce que c'est, pourquoi il confond tellement les gens et ce qu'il faut faire à ce sujet. Livre blanc sur l'alias strict . En particulier, il explique pourquoi les unions sont un comportement risqué pour C ++ et pourquoi l'utilisation de memcpy est le seul correctif portable à la fois en C et C ++. J'espère que cela vous sera utile.

phorgan1
la source
3
" Un aliasing strict ne fait pas uniquement référence aux pointeurs, il affecte également les références " En fait, il fait référence aux valeurs . " utiliser memcpy est le seul correctif portable " Hear!
curiousguy
5
Bon papier. Mon point de vue: (1) ce «problème» de pseudonyme est une réaction excessive à une mauvaise programmation - en essayant de protéger le mauvais programmeur de ses mauvaises habitudes. Si le programmeur a de bonnes habitudes, cet aliasing n'est qu'une nuisance et les contrôles peuvent être désactivés en toute sécurité. (2) L'optimisation côté compilateur ne doit être effectuée que dans des cas bien connus et doit en cas de doute suivre strictement le code source; forcer le programmeur à écrire du code pour répondre aux particularités du compilateur est, tout simplement, faux. Pire encore à faire partie de la norme.
slashmais
4
@slashmais (1) " est une réaction excessive à une mauvaise programmation " Nonsense. C'est un rejet des mauvaises habitudes. Vous faites cela? Vous en payez le prix: aucune garantie pour vous! (2) Des cas bien connus? Lesquels? La règle stricte d'alias devrait être "bien connue"!
curiousguy
5
@curiousguy: Après avoir éliminé quelques points de confusion, il est clair que le langage C avec les règles d'alias empêche les programmes d'implémenter des pools de mémoire agnostiques de type. Certains types de programmes peuvent se débrouiller avec malloc / free, mais d'autres ont besoin d'une logique de gestion de la mémoire mieux adaptée aux tâches à accomplir. Je me demande pourquoi la justification C89 a utilisé un exemple aussi minable de la raison de la règle d'aliasing, car leur exemple donne l'impression que la règle ne posera pas de difficulté majeure pour effectuer une tâche raisonnable.
supercat
5
@curiousguy, la plupart des suites de compilateurs incluent -fstrict-aliasing par défaut sur -O3 et ce contrat caché est imposé aux utilisateurs qui n'ont jamais entendu parler du TBAA et ont écrit du code comme le ferait un programmeur système. Je ne veux pas paraître faux aux programmeurs système, mais ce type d'optimisation devrait être laissé en dehors de l'option par défaut de -O3 et devrait être une optimisation opt-in pour ceux qui savent ce qu'est TBAA. Il n'est pas amusant de regarder le «bogue» du compilateur qui s'avère être un code utilisateur violant TBAA, en particulier le suivi de la violation du niveau source dans le code utilisateur.
kchoi
34

En complément de ce que Doug T. a déjà écrit, voici 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;
}

Compilez avec gcc -O2 -o check check.c. Habituellement (avec la plupart des versions de gcc que j'ai essayées), cela génère un "problème d'alias strict", car 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 le if (*h == 5)away 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.

Ingo Blackman
la source
si vous ajoutez un deuxième court * j pour vérifier () et l'utiliser (* j = 7), l'optimisation disparaîtra car ggc ne le fait pas si h et j ne pointent pas réellement vers la même valeur. oui l'optimisation est vraiment intelligente.
philippe lhardy
2
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*et int64_t*). On pourrait s'attendre à ce qu'un compilateur sensé reconnaisse que a long long*et int64_t*puisse accéder au même stockage s'ils sont stockés de manière identique, mais un tel traitement n'est plus à la mode.
supercat
Grr ... x64 est une convention Microsoft. Utilisez plutôt amd64 ou x86_64.
SS Anne
Grr ... x64 est une convention Microsoft. Utilisez plutôt amd64 ou x86_64.
SS Anne
17

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

Chris Jester-Young
la source
1
Voir ma réponse ici pour les citations pertinentes, en particulier les notes de bas de page, mais le type de punition par le biais des syndicats a toujours été autorisé en C, même s'il était mal formulé au début. Vous voulez clarifier votre réponse.
Shafik Yaghmour
@ShafikYaghmour: C89 a clairement permis aux implémenteurs de sélectionner les cas dans lesquels ils reconnaîtraient ou ne reconnaîtraient pas utilement la punition de type via les unions. Une implémentation pourrait, par exemple, spécifier que pour qu'une écriture sur un type suivie d'une lecture sur un autre soit reconnue comme punition de type, si le programmeur a effectué l'une des opérations suivantes entre l'écriture et la lecture : (1) évaluer une valeur l contenant le type de syndicat [en prenant l'adresse d'un membre serait admissible, s'il est fait au bon moment dans la séquence]; (2) convertir un pointeur d'un type en un pointeur de l'autre et accéder via ce ptr.
supercat
@ShafikYaghmour: Une implémentation pourrait également spécifier, par exemple, que le type entre les valeurs entières et à virgule flottante ne fonctionnerait de manière fiable que si le code exécutait une fpsync()directive entre l'écriture comme fp et la lecture comme int ou vice versa [sur les implémentations avec des pipelines et des caches FPU et entiers séparés , une telle directive pourrait être coûteuse, mais pas aussi coûteuse que de demander au compilateur d'effectuer une telle synchronisation sur chaque accès à l'union]. Ou une implémentation pourrait spécifier que la valeur résultante ne sera jamais utilisable sauf dans des circonstances utilisant des séquences initiales communes.
supercat
@ShafikYaghmour: Sous C89, les implémentations pouvaient interdire la plupart des formes de punition de type, y compris via les unions, mais l'équivalence entre les pointeurs vers les syndicats et les pointeurs vers leurs membres impliquait que la punition de type était autorisée dans les implémentations qui ne l' interdisaient pas expressément .
supercat
17

Selon la justification 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 requis pour recharger la valeur de xentre l'affectation et l'instruction return afin de tenir compte de la possibilité qui ppourrait pointer vers x, et l'affectation à *ppourrait par conséquent modifier la valeur de x. L'idée qu'un compilateur devrait avoir le droit de présumer qu'il n'y aura pas d'alias dans des situations comme celles ci-dessus n'était pas controversée.

Malheureusement, les auteurs du C89 ont écrit leur règle d'une manière qui, si elle était lue littéralement, inciterait même la fonction suivante à invoquer un comportement indéfini:

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

car il utilise une valeur l de type intpour accéder à un objet de type struct Set intne fait pas partie des types pouvant être utilisés pour accéder à a struct S. Parce qu'il serait absurde de traiter toute utilisation de membres non structurés de structures et d'unions comme un comportement indéfini, presque tout le monde reconnaît qu'il existe au moins certaines circonstances où une valeur l 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 sont ces circonstances.

Une grande partie du problème est le résultat du rapport de défaut # 028, qui a posé des questions sur le 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éfauts # 28 indique que le programme appelle un comportement indéfini car l'action d'écrire un membre d'union de type "double" et de lire un membre de type "int" appelle un comportement défini par l'implémentation. Un tel raisonnement n'a pas de sens, mais constitue la base des règles de type effectif qui compliquent inutilement le langage tout en ne faisant rien pour résoudre le problème d'origine.

La meilleure façon de résoudre le problème d'origine serait probablement de traiter la note de bas de page sur le but de la règle comme si elle était normative et de la rendre inapplicable, sauf dans les cas qui impliquent réellement des accès conflictuels à l'aide d'alias. Étant 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 à l'intérieur inc_intparce que tous les accès au stockage accédé via *pse font avec une valeur de type l int, et il n'y a pas de conflit testparce qu'il pest visiblement dérivé d'un struct S, et à la prochaine sutilisation, tous les accès à ce stockage qui seront jamais effectués à travers paura 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 existe un conflit d'alias entre pet l'accès à s.xla ligne marquée car à ce stade de l'exécution, il existe une autre référence qui sera utilisée pour accéder au même stockage .

Si le rapport de défaut 028 avait indiqué que l'exemple d'origine 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 efficaces" ou une autre complexité de ce type.

supercat
la source
Eh bien, il serait intéressant de lire une sorte de proposition qui était plus ou moins «ce que le comité des normes aurait pu faire» qui atteignait ses objectifs sans introduire autant de complexité.
2018
1
@jrh: Je pense que ce serait assez simple. Reconnaissez que 1. Pour que l'alias se produise lors d'une exécution particulière d'une fonction ou d'une boucle, deux pointeurs ou valeurs différentes doivent être utilisés pendant cette exécution pour adresser le même stockage dans une mode en conflit; 2. Reconnaître que dans des contextes où un pointeur ou une valeur est nouvellement visiblement dérivé d'un autre, un accès au second est un accès au premier; 3. Reconnaissez que la règle n'est pas destinée à s'appliquer dans les cas qui n'impliquent pas réellement d'alias.
supercat
1
Les circonstances exactes où un compilateur reconnaît une valeur l nouvellement dérivée peuvent être un problème de qualité de mise en œuvre, mais tout compilateur décent à distance devrait être capable de reconnaître les formes que gcc et clang ignorent délibérément.
supercat
11

Après avoir lu bon nombre des réponses, je ressens le besoin d'ajouter quelque chose:

Un aliasing strict (que je décrirai un peu) est important car :

  1. L'accès à la mémoire peut être coûteux (en termes de performances), c'est pourquoi les données sont manipulées dans les registres du processeur avant d'être réécrites dans la mémoire physique.

  2. Si les données de deux registres CPU différents seront écrites dans le même espace mémoire, nous ne pouvons pas prédire quelles données "survivront" lorsque nous coderons en C.

    En assemblage, où nous codons le chargement et le déchargement des registres CPU manuellement, 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 un code complexe qui gère les collisions possibles .

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

La règle d'alias stricte nous permet d'éviter le code machine redondant dans les cas où il devrait être sûr de supposer que deux pointeurs ne pointent pas vers le même bloc de mémoire (voir aussi le restrictmot - clé).

L'aliasing strict indique qu'il est sûr de supposer que les pointeurs vers différents types pointent vers différents emplacements 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 supposera que l'adresse mémoire est différente et ne protégera pas contre les collisions d'adresses mémoire, ce qui accélérera le code machine.

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 du processeur, de sorte que le code puisse se retrouver comme suit:

  1. charger aet bde la mémoire.

  2. ajouter aà b.

  3. enregistrer b et recharger a .

    (sauvegarde du registre CPU dans la mémoire et chargement de la mémoire dans le registre CPU).

  4. ajouter bà a.

  5. enregistrer a(du registre CPU) dans 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 se protéger contre les instances où aet bpointer vers la même adresse mémoire.

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

  1. Cela peut être dit au compilateur de deux manières, en utilisant différents types pour pointer. c'est à dire:

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

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

Désormais, en satisfaisant à la règle de repli strict, l'étape 3 peut être évitée et le code s'exécutera beaucoup plus rapidement.

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

  1. charger aet bde la mémoire.

  2. ajouter aà b.

  3. enregistrer le résultat vers aet vers b.

Cette optimisation n'aurait pas pu être effectuée auparavant, en raison de la collision possible (où aet bserait triplé au lieu de doublé).

Myst
la source
avec le mot-clé restrict, à l'étape 3, ne devrait-il pas être uniquement enregistré le résultat en «b»? Il semble que le résultat de la sommation soit également enregistré dans «a». Doit-il «b» être rechargé à nouveau?
NeilB
1
@NeilB - Yap vous avez raison. Nous ne faisons qu'économiser b(pas le recharger) et recharger a. J'espère que c'est plus clair maintenant.
Myst
L'alias basé sur le type peut avoir offert certains avantages avant restrict, mais je pense que ce dernier serait dans la plupart des cas plus efficace, et le relâchement de certaines contraintes registerlui permettrait de remplir certains des cas où restrictcela ne serait pas utile. Je ne suis pas sûr qu'il ait jamais été "important" de traiter la norme comme décrivant pleinement tous les cas où les programmeurs devraient s'attendre à ce que les compilateurs reconnaissent les preuves d'alias, plutôt que de simplement décrire les endroits où les compilateurs doivent présumer l'alias même lorsqu'aucune preuve particulière n'existe .
supercat
Notez que bien que le chargement à partir de la RAM principale soit très lent (et peut bloquer le cœur du processeur pendant une longue période si les opérations suivantes dépendent du résultat), le chargement à partir du cache L1 est assez rapide, tout comme l'écriture sur une ligne de cache qui écrivait récemment. par le même noyau. Donc, tout sauf la première lecture ou écriture à une adresse sera généralement assez rapide: la différence entre l'accès à l'adr reg / mem est plus petite que la différence entre l'adr. Mem mis en cache / non mis en cache.
curiousguy
@curiousguy - bien que vous ayez raison, "rapide" dans ce cas est relatif. Le cache L1 est probablement encore un ordre de grandeur plus lent que les registres CPU (je pense que plus de 10 fois plus lent). De plus, le restrictmot - clé minimise non seulement la vitesse des opérations mais aussi leur nombre, ce qui pourrait être significatif ... Je veux dire, après tout, l'opération la plus rapide n'est pas une opération du tout :)
Myst
6

Un alias strict n'autorise pas différents types de pointeurs vers les mêmes données.

Cet article devrait vous aider à comprendre le problème en détail.

Jason Dagit
la source
4
Vous pouvez également créer un alias entre les références et entre une référence et un pointeur. Voir mon tutoriel dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1
4
Il est permis d'avoir différents types de pointeurs vers les mêmes données. Lorsque l'aliasing strict entre en jeu, c'est lorsque le même emplacement mémoire est écrit via un type de pointeur et lu dans un autre. En outre, certains types différents sont autorisés (par exemple, intet une structure qui contient un int).
MM
-3

Techniquement en C ++, la règle d'aliasing stricte n'est probablement jamais applicable.

Notez la définition de l'indirection ( opérateur * ):

L'opérateur unaire * effectue une indirection: l'expression à laquelle il est appliqué doit être un pointeur vers un type d'objet, ou un pointeur vers un type de fonction et le résultat est une valeur l se référant à l'objet ou à la fonction vers laquelle l'expression pointe .

Également de la définition de glvalue

Une glvalue est une expression dont l'évaluation détermine l'identité d'un objet, (... snip)

Ainsi, dans toute trace de programme bien définie, une valeur gl fait référence à un objet. Donc, la soi-disant règle d'aliasing stricte ne s'applique jamais. Ce n'est peut-être pas ce que les concepteurs voulaient.

curiousguy
la source
4
La norme C utilise le terme «objet» pour désigner un certain nombre de concepts différents. Parmi eux, une séquence d'octets qui sont exclusivement alloués à un but, une référence sans nécessairement exclusive à une séquence d'octets à / à partir de laquelle une valeur d'un type particulier pourrait être écrit ou lu, ou une telle référence fait a été ou sera accessible dans un certain contexte. Je ne pense pas qu'il existe un moyen sensé de définir le terme "objet" qui soit cohérent avec la façon dont le Standard l'utilise.
supercat
@supercat Incorrect. Malgré votre imagination, il est en fait assez cohérent. Dans ISO C, il est défini comme "région de stockage de données dans l'environnement d'exécution, dont le contenu peut représenter des valeurs". Dans ISO C ++, il existe une définition similaire. Votre commentaire est encore plus hors de propos que la réponse car tout ce que vous avez mentionné sont des moyens de représentation pour référencer le contenu des objets , tandis que la réponse illustre le concept C ++ (glvalue) d'une sorte d'expressions étroitement liées à l' identité des objets. Et toutes les règles d'alias sont pertinentes pour l'identité mais pas pour le contenu.
FrankHB
1
@FrankHB: Si l'on déclare int foo;, à quoi accède l'expression lvalue *(char*)&foo? Est-ce un objet de type char? Cet objet existe-t-il en même temps que foo? Est-ce que l'écriture foochangerait la valeur stockée de cet objet de type susmentionné char? Dans l'affirmative, existe-t-il une règle permettant d' characcéder à la valeur stockée d'un objet de type à l'aide d'une valeur de type l int?
supercat
@FrankHB: En l'absence de 6.5p7, on pourrait simplement dire que chaque région de stockage contient simultanément tous les objets de tous types qui pourraient tenir dans cette région de stockage, et que l'accès à cette région de stockage accède simultanément à tous. Interpréter de cette manière l'utilisation du terme "objet" dans 6.5p7, cependant, interdirait de faire quoi que ce soit avec des valeurs de type non-caractère, ce qui serait clairement un résultat absurde et irait à l'encontre du but de la règle. De plus, le concept "d'objet" utilisé partout ailleurs que 6.5p6 a un type de compilation statique, mais ...
supercat
1
sizeof (int) est 4, la déclaration fait-elle int i; crée-t-elle quatre objets de chaque type de caractère in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both * (char *) & i` et i. Enfin, rien dans la norme ne permet même à un volatilepointeur qualifié d'accéder à des registres matériels qui ne répondent pas à la définition d '"objet".
supercat