web-dev-qa-db-fra.com

Syndicats, aliasing et punition de type dans la pratique: qu'est-ce qui fonctionne et qu'est-ce qui ne fonctionne pas?

J'ai du mal à comprendre ce qui peut et ne peut pas être fait en utilisant les syndicats avec GCC. J'ai lu les questions (en particulier ici et ici ) à ce sujet mais elles se concentrent sur le standard C++, je pense qu'il y a un décalage entre le standard C++ et la pratique (le plus couramment utilisé compilateurs).

En particulier, j'ai récemment trouvé des informations confuses dans le GCC online doc en lisant sur l'indicateur de compilation - fstrict-aliasing . Ça dit:

-fstrict-aliasing

Autorisez le compilateur à assumer les règles d'alias les plus strictes applicables à la langue en cours de compilation. Pour C (et C++), cela active les optimisations basées sur le type d'expressions. En particulier, un objet d'un type est supposé ne jamais résider à la même adresse qu'un objet d'un type différent, sauf si les types sont presque les mêmes. Par exemple, un unsigned int peut alias un int, mais pas un void* ou un double. Un type de caractère peut alias tout autre type. Portez une attention particulière au code comme celui-ci:

union a_union {
  int i;
  double d;
};

int f() {
  union a_union t;
  t.d = 3.0;
  return t.i;
}

La pratique de lire à partir d'un autre membre du syndicat que celui sur lequel on a écrit le plus récemment (appelé "punition de type") est courante. Même avec -fstrict-aliasing, la punition de type est autorisée, à condition que la mémoire soit accessible via le type d'union. Ainsi, le code ci-dessus fonctionne comme prévu.

Voici ce que je pense avoir compris de cet exemple et de mes doutes:

1) l'alias fonctionne uniquement entre des types similaires, ou char

Conséquence de 1): l'aliasing - comme le suggère le mot - c'est quand vous avez une valeur et deux membres pour y accéder (c'est-à-dire les mêmes octets);

Doute: deux types sont-ils similaires quand ils ont la même taille en octets? Sinon, quels sont les types similaires?

Conséquence de 1) pour les types non similaires (quoi que cela signifie), l'aliasing ne fonctionne pas;

2) la punition de type est lorsque nous lisons un membre différent de celui auquel nous avons écrit; il est courant et fonctionne comme prévu tant que la mémoire est accessible via le type d'union;

Doute: aliasing un cas spécifique de punition de type où les types sont similaires?

Je suis confus car il dit que int et double non signés ne sont pas similaires, donc l'alias ne fonctionne pas; puis dans l'exemple, il fait un alias entre int et double et il indique clairement que cela fonctionne comme prévu, mais l'appelle punition de type: non pas parce que les types sont ou ne sont pas similaires, mais parce qu'il lit à partir d'un membre qu'il n'a pas écrit. Mais la lecture d'un membre qu'il n'a pas écrit est ce à quoi je croyais que l'alias était (comme le suggère la Parole). Je suis perdu.

Les questions: quelqu'un peut-il clarifier la différence entre l'aliasing et le type-punning et quelles utilisations des deux techniques fonctionnent comme prévu dans GCC? Et que fait le drapeau du compilateur?

16
L.C.

L'aliasing peut être pris littéralement pour ce qu'il signifie: c'est lorsque deux expressions différentes se réfèrent au même objet. La punition de type consiste à "pun" un type, c'est-à-dire à utiliser un objet d'un certain type comme un type différent.

Formellement, la punition de type est un comportement indéfini à quelques exceptions près. Cela se produit généralement lorsque vous manipulez des morceaux avec négligence

int mantissa(float f)
{
    return (int&)f & 0x7FFFFF;    // Accessing a float as if it's an int
}

Les exceptions sont (simplifiées)

  • Accès aux entiers en tant que leurs homologues non signés/signés
  • Accéder à n'importe quoi en tant que char, unsigned char ou std::byte

C'est ce que l'on appelle la règle d'alias strict: le compilateur peut supposer en toute sécurité que deux expressions de types différents ne font jamais référence au même objet (à l'exception des exceptions ci-dessus) car elles auraient autrement un comportement non défini. Cela facilite les optimisations telles que

void transform(float* dst, const int* src, int n)
{
    for(int i = 0; i < n; i++)
        dst[i] = src[i];    // Can be unrolled and use vector instructions
                            // If dst and src alias the results would be wrong
}

Ce que dit gcc, c'est qu'il assouplit un peu les règles et permet la saisie de type par le biais des unions même si la norme ne l'exige pas.

union {
    int64_t num;
    struct {
        int32_t hi, lo;
    } parts;
} u = {42};
u.parts.hi = 420;

C'est les garanties gcc de type-pun qui fonctionneront. D'autres cas peuvent sembler fonctionner mais peuvent un jour être brisés en silence.

9
Passer By

La terminologie est une bonne chose, je peux l'utiliser comme je veux, tout comme les autres!

deux types sont-ils similaires lorsqu'ils ont la même taille en octets? Sinon, quels sont les types similaires?

En gros, les types sont similaires lorsqu'ils diffèrent par la constance ou la signature. La taille en octets seule n'est certainement pas suffisante.

l'aliasing est-il un cas spécifique de punition de type où les types sont similaires?

La punition de type est une technique qui contourne le système de type.

Le crénelage est un cas spécifique de celui qui implique de placer des objets de différents types à la même adresse. Le crénelage est généralement autorisé lorsque les types sont similaires et interdit dans le cas contraire. De plus, on peut accéder à un objet de tout type via une valeur char (ou similaire à char), mais en faisant le contraire (c'est-à-dire accéder à un objet de type char via un type différent de lvalue) n'est pas autorisé. Ceci est garanti par les normes C et C++, GCC implémente simplement ce que les normes mandatent.

La documentation du CCG semble utiliser le "type punning" dans un sens étroit de lecture d'un membre du syndicat autre que celui auquel il a été écrit en dernier. Ce type de punition de type est autorisé par la norme C même lorsque les types ne sont pas similaires. OTOH la norme C++ ne le permet pas. GCC peut ou non étendre l'autorisation à C++, la documentation n'est pas claire à ce sujet.

Sans pour autant -fstrict-aliasing, GCC assouplit apparemment ces exigences, mais il n'est pas clair dans quelle mesure exacte. Notez que -fstrict-aliasing est la valeur par défaut lors de l'exécution d'une construction optimisée.

En bout de ligne, il suffit de programmer à la norme. Si GCC assouplit les exigences de la norme, elle n'est pas significative et ne vaut pas la peine.

4
n. 'pronouns' m.

Selon la note de bas de page 88 du projet C11 N1570, la "règle stricte d'aliasing" (6.5p7) vise à spécifier les circonstances dans lesquelles les compilateurs doivent permettre la possibilité que les choses puissent être alias, mais n'essaye pas de définir quel aliasing - est. Quelque part le long de la ligne, une croyance populaire a émergé selon laquelle les accès autres que ceux définis par la règle représentent un "aliasing", et ceux autorisés ne le sont pas, mais en fait, c'est le contraire.

Étant donné une fonction comme:

int foo(int *p, int *q)
{ *p = 1; *q = 2; return *p; }

La section 6.5p7 ne dit pas que p et q ne seront pas alias s'ils identifient le même stockage. Il spécifie plutôt qu'ils sont autorisés à l'alias.

Notez que toutes les opérations qui impliquent l'accès au stockage d'un type comme d'un autre ne représentent pas un alias. Une opération sur une valeur l qui vient d'être visiblement dérivée d'un autre objet ne "alias" pas cet autre objet. Au lieu de cela, il est une opération sur cet objet. L'aliasing se produit si, entre le moment où une référence à un stockage est créé et le moment où il est utilisé, le même stockage est référencé d'une manière ou d'une autre non dérivé du premier, ou le code entre dans un contexte dans lequel cela se produit .

Bien que la capacité de reconnaître quand une valeur l est dérivée d'une autre soit un problème de qualité de mise en œuvre, les auteurs de la norme doivent s'attendre à ce que les mises en œuvre reconnaissent certaines constructions au-delà de celles prescrites. Il n'y a aucune autorisation générale pour accéder au stockage associé à une structure ou à une union en utilisant une valeur l de type membre, et rien dans la norme explicitement disons qu'une opération impliquant someStruct.member doit être reconnu comme une opération sur un someStruct. Au lieu de cela, les auteurs de la Norme s'attendaient à ce que les rédacteurs de compilateurs qui font un effort raisonnable pour soutenir les constructions dont leurs clients ont besoin soient mieux placés que le Comité pour juger les besoins de ces clients et les satisfaire. Étant donné que tout compilateur qui fait un effort raisonnable à distance pour reconnaître les références dérivées remarquera que someStruct.member est dérivé de someStruct, les auteurs de la norme n'ont pas vu la nécessité de le rendre explicitement obligatoire.

Malheureusement, le traitement des constructions comme:

actOnStruct(&someUnion.someStruct);
int q=*(someUnion.intArray+i)

a évolué à partir de "Il est suffisamment évident que actOnStruct et la déréférence du pointeur doivent agir sur someUnion (et par conséquent tous ses membres) pour qu'il ne soit pas nécessaire d'imposer un tel comportement" à "Depuis" la norme n'exige pas que les implémentations reconnaissent que les actions ci-dessus peuvent affecter someUnion, tout code reposant sur un tel comportement est rompu et n'a pas besoin d'être pris en charge ". Aucune des constructions ci-dessus n'est prise en charge de manière fiable par gcc ou clang, sauf dans -fno-strict-aliasing mode, même si la plupart des "optimisations" qui seraient bloquées en les supportant généreraient du code "efficace" mais inutile.

Si vous utilisez -fno-strict-aliasing sur tout compilateur ayant une telle option, presque tout fonctionnera. Si vous utilisez -fstrict-aliasing sur icc, il essaiera de prendre en charge les constructions qui utilisent la punition de type sans alias, bien que je ne sache pas s'il existe de la documentation sur les constructions qu'il gère ou ne gère pas. Si tu utilises -fstrict-aliasing sur gcc ou clang, tout ce qui fonctionne est purement fortuit.

2
supercat

Dans ANSI C (AKA C89), vous avez (section 3.3.2.3 Structure et membres du syndicat):

si un membre d'un objet union est accédé après qu'une valeur a été stockée dans un autre membre de l'objet, le comportement est défini par l'implémentation

Dans C99, vous avez (section 6.5.2.3 Structure et membres du syndicat):

Si le membre utilisé pour accéder au contenu d'un objet union n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation d'objet de la valeur est réinterprétée en tant que représentation d'objet dans le nouveau type comme décrit en 6.2.6 (un processus parfois appelé "type punning"). Cela pourrait être une représentation piège.

IOW, le punning de type union est autorisé en C, bien que la sémantique réelle puisse être différente, selon la norme de langage prise en charge (notez que la sémantique C99 est plus étroite que la C89 définie par l'implémentation).

En C99, vous avez également (section 6.5 Expressions):

Un objet doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui a l'un des types 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, récursivement, un membre d'une union sous-agrégée ou contenue), ou

- un type de caractère.

Et il y a une section (6.2.7 Type compatible et type composite) dans C99 qui décrit les types compatibles:

Deux types ont un type compatible si leurs types sont identiques. Des règles supplémentaires permettant de déterminer si deux types sont compatibles sont décrites au 6.7.2 pour les spécificateurs de type, au 6.7.3 pour les qualificateurs de type et au 6.7.5 pour les déclarants. ...

Et puis (6.7.5.1 Déclarateurs de pointeur):

Pour que deux types de pointeurs soient compatibles, les deux doivent être qualifiés de manière identique et les deux doivent être des pointeurs vers des types compatibles.

Pour simplifier un peu, cela signifie qu'en C, en utilisant un pointeur, vous pouvez accéder aux entrées signées en tant qu'entités non signées (et vice versa) et vous pouvez accéder à des caractères individuels dans n'importe quoi. Tout autre élément équivaudrait à une violation d'alias.

Vous pouvez trouver un langage similaire dans les différentes versions de la norme C++. Cependant, pour autant que je puisse voir en C++ 03 et C++ 11, le punning basé sur l'union n'est pas explicitement autorisé (contrairement à C).

2
Alexey Frunze

Je pense qu'il est bon d'ajouter une réponse complémentaire, tout simplement parce que lorsque j'ai posé la question, je ne savais pas comment répondre à mes besoins sans utiliser UNION: je me suis obstiné à l'utiliser parce qu'il semblait répondre précisément à mes besoins.

La bonne façon de faire le punning de type et d'éviter les conséquences possibles d'un comportement non défini (selon le compilateur et d'autres paramètres d'env.) Est d'utiliser std :: memcpy et de copier les octets de mémoire d'un type à un autre. Ceci est expliqué - par exemple - ici et ici .

J'ai également lu que souvent lorsqu'un compilateur produit du code valide pour la punition de type à l'aide d'unions, il produit le même code binaire que si std :: memcpy était utilisé.

Enfin, même si ces informations ne répondent pas directement à ma question d'origine, elles sont si étroitement liées que j'ai pensé qu'il était utile de les ajouter ici.

0
L.C.