gcc, aliasing strict, et la diffusion à travers une union

avez-vous des histoires d'horreur à raconter? Le manuel GCC a récemment ajouté un avertissement concernant-fstrict-aliasing et lancer un pointeur à travers une union:

[...] En prenant l'adresse, en moulant le pointeur résultant et en déréférenciant le résultat a comportement non défini [soulignement ajouté], même si la fonte utilise un type d'union, p.ex.:

    union a_union {
        int i;
        double d;
    };

    int f() {
        double d = 3.0;
        return ((union a_union *)&d)->i;
    }

Does quelqu'un aurait un exemple pour illustrer ce comportement indéfini?

Note cette question Est et non au sujet de ce que dit ou ne dit pas la norme C99. Il s'agit du fonctionnement réel de gcc , et d'autres compilateurs existants, aujourd'hui.

Je ne fais que supposer, mais un problème potentiel pourrait résider dans le réglage de d à 3.0. Parce que d est une variable temporaire qui n'est jamais directement lire, et qui n'est jamais lu via un pointeur "quelque peu compatible", le compilateur peut ne pas prendre la peine de le configurer. Et ensuite f () retournera quelques déchets de la cheminée.

ma simple, naïve, tentative échoue. Par exemple:

#include <stdio.h>

union a_union {
    int i;
    double d;
};

int f1(void) {
    union a_union t;
    t.d = 3333333.0;
    return t.i; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3)
}

int f2(void) {
    double d = 3333333.0;
    return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior' 
}

int main(void) {
    printf("%dn", f1());
    printf("%dn", f2());
    return 0;
}

fonctionne très bien, donnant sur CYGWIN:

-2147483648
-2147483648

en regardant l'assembleur, nous voyons que gcc optimise complètement t loin: f1() stocke simplement le réponse pré-calculée:

movl    $-2147483648, %eax

alors que f2() pousse 3333333.0 sur la pile floating-point , puis extrait la valeur de retour:

flds   LC0                 # LC0: 1246458708 (= 3333333.0) (--> 80 bits)
fstpl  -8(%ebp)            # save in d (64 bits)
movl   -8(%ebp), %eax      # return value (32 bits)

et les fonctions sont aussi inlined (ce qui semble être la cause de quelques bogues de mise sous-pseudo-stricte) mais cela n'est pas pertinent ici. (Et ce assembleur n'est pas pertinent, mais il ajoute des détails.)

également noter que prendre des adresses est évidemment erroné (ou bon , si vous essayez d'illustrer un comportement non défini). Par exemple, tout comme nous savons que c'est mal:

extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior

nous savons aussi que c'est mal:

extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior

pour une discussion générale à ce sujet, voir par exemple:

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf

http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html

http://davmac.wordpress.com/2010/02/26/c99-revisited/

http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

( = page de recherche sur Google puis afficher la page en cache )

Qu'est-ce que le strict l'aliasing de la règle?

C99 règles d'alias strictes en C++ (GCC)

dans le premier lien, projet de procès-verbal d'une réunion de L'ISO il y a sept mois, un participant note à la section 4.16:

Est-il quelqu'un qui pense que les règles sont assez claires? Personne n'est vraiment capable de les interpréter.

autres notes: mon test était avec gcc 4.3.4, avec -O2; options-O2 et-O3 impliquent-fstrict-aliasing. L'exemple du manuel GCC suppose sizeof(double) >= sizeof (int); peu importe s'ils sont inégaux.

aussi, comme L'a noté Mike Acton dans le lien cellperformace, -Wstrict-aliasing=2 , mais pas =3 , produit warning: dereferencing type-punned pointer might break strict-aliasing rules pour l'exemple ici.

32
demandé sur curiousguy 2010-05-25 20:06:42

7 réponses

le fait que GCC avertisse sur les syndicats ne pas nécessairement signifie que les syndicats ne travaillent pas actuellement. Mais voici un exemple un peu moins simple que le vôtre:

#include <stdio.h>

struct B {
    int i1;
    int i2;
};

union A {
    struct B b;
    double d;
};

int main() {
    double d = 3.0;
    #ifdef USE_UNION
        ((union A*)&d)->b.i2 += 0x80000000;
    #else
        ((int*)&d)[1] += 0x80000000;
    #endif
    printf("%g\n", d);
}

sortie:

$ gcc --version
gcc (GCC) 4.3.4 20090804 (release) 1
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -oalias alias.c -O1 -std=c99 && ./alias
-3

$ gcc -oalias alias.c -O3 -std=c99 && ./alias
3

$ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias
-3

$ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias
-3

ainsi sur GCC 4.3.4, l'union" sauve le jour "(en supposant que je veux la sortie"-3"). Il désactive l'optimisation qui repose sur un aliasing strict et qui aboutit à la sortie "3" dans le second cas (seulement). Avec -Mur, USE_UNION désactive également l'avertissement de type-jeu de mots.

Je n'ai pas gcc 4.4 à tester, mais s'il vous plaît donnez ce code un essai. Votre code en effet teste si la mémoire de d est initialisée avant de la relire à travers une union: le mien teste si elle est modifiée.

Btw, la manière la plus sûre de lire la moitié d'un double comme un int est:

double d = 3;
int i;
memcpy(&i, &d, sizeof i);
return i;

avec optimisation sur GCC, il en résulte:

    int thing() {
401130:       55                      push   %ebp
401131:       89 e5                   mov    %esp,%ebp
401133:       83 ec 10                sub    "151930920"x10,%esp
        double d = 3;
401136:       d9 05 a8 20 40 00       flds   0x4020a8
40113c:       dd 5d f0                fstpl  -0x10(%ebp)
        int i;
        memcpy(&i, &d, sizeof i);
40113f:       8b 45 f0                mov    -0x10(%ebp),%eax
        return i;
    }
401142:       c9                      leave
401143:       c3                      ret

So il n'y a pas d'appel à memcpy. Si vous ne faites pas cela, vous méritez ce que vous obtenez si l'union cesse de travailler dans le GCC ; -)

10
répondu Steve Jessop 2010-06-02 15:17:01

votre affirmation que le code suivant est "erroné":

extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior

... est faux. Le simple fait de prendre l'adresse des deux membres du syndicat et de les transmettre à une fonction externe ne se traduit pas par un comportement non défini; on ne l'obtient qu'en négligeant un de ces pointeurs d'une manière non valable. Par exemple, si la fonction foo retourne immédiatement sans déréférencer les pointeurs que vous lui avez passés, alors le comportement n'est pas indéfini. Avec une lecture stricte de la C99 standard, il y a même des cas où les pointeurs peuvent être déréférencés sans invoquer un comportement non défini; par exemple, il pourrait lire la valeur référencée par le second pointeur, puis stocker une valeur à travers le premier pointeur, tant qu'ils pointent tous les deux vers un objet dynamiquement alloué (c.-à-d. un sans un "type déclaré").

4
répondu davmac 2017-10-11 12:30:57

Aliasing se produit lorsque le compilateur a deux pointeurs différents pour le même morceau de mémoire. En tapant un pointeur, vous générez un nouveau pointeur temporaire. Si l'optimiseur réordonne les instructions d'assemblage par exemple, l'accès aux deux pointeurs peut donner deux résultats totalement différents - il peut réordonner une lecture avant une écriture à la même adresse. C'est pourquoi il est un comportement indéfini.

vous êtes peu probable de voir le problème dans le code de test très simple, mais il apparaîtra quand il se passera beaucoup de choses.

je pense que l'avertissement est d'indiquer clairement que les syndicats ne sont pas un cas spécial, même si vous pouvez vous attendre à ce qu'ils le soient.

voir cet article de Wikipedia pour plus d'informations sur l'alias: http://en.wikipedia.org/wiki/Aliasing_ (computing)#Conflicts_with_optimization

3
répondu Mark Ransom 2010-05-25 16:33:41

Eh bien c'est un peu de Necro-posting, Mais voici une histoire d'horreur. Je porte un programme qui a été écrit avec l'hypothèse que l'ordre des bytes natifs est big endian. Maintenant j'en ai besoin pour travailler sur little endian aussi. Malheureusement, je ne peux pas simplement utiliser l'ordre des octets natifs partout, car les données peuvent être consultées de bien des façons. Par exemple, un entier de 64 bits pourrait être traité comme deux entiers de 32 bits ou comme 4 entiers de 16 bits, ou même comme 16 entiers de 4 bits. Pour empirer les choses, il n'y a aucun moyen de comprendre ce qui est stocké exactement en mémoire, parce que le logiciel est un interprète pour une sorte de code octet, et les données sont formées par ce code octet. Par exemple, le code octet peut contenir des instructions pour écrire un tableau d'entiers 16 bits, puis accéder à une paire d'entre eux comme un flotteur 32 bits. Et il n'y a aucun moyen de prédire ou de modifier le code octet.

par conséquent, j'ai dû créer un ensemble de classes d'enrubannage pour fonctionner avec des valeurs stockées dans le grand ordre endian indépendamment de la natif boutisme. Fonctionne parfaitement dans Visual Studio et dans GCC sur Linux sans optimisations. Mais avec gcc -O2, l'enfer s'est déchaîné. Après beaucoup de débogage, j'ai compris que la raison était ici:

double D;
float F; 
Ul *pF=(Ul*)&F; // Ul is unsigned long
*pF=pop0->lu.r(); // r() returns Ul
D=(double)F; 

ce code a été utilisé pour convertir une représentation 32 bits d'un flotteur stocké dans un entier 32 bits en double. Il semble que le compilateur ait décidé de faire l'assignation à *pF après l'assignation à D - le résultat a été que la première fois que le code a été exécuté, le la valeur de D était un déchet, et les valeurs résultantes étaient "tardives" d'une itération.

Miraculeusement, il n'y avait pas d'autres problèmes à ce point. J'ai donc décidé d'aller de l'avant et de tester mon nouveau code sur la plate-forme originale, HP-UX sur un processeur RISC avec un ordre natif big endian. Il s'est cassé à nouveau, cette fois dans ma nouvelle classe:

typedef unsigned long long Ur; // 64-bit uint
typedef unsigned char Uc;
class BEDoubleRef {
        double *p;
public:
        inline BEDoubleRef(double *p): p(p) {}
        inline operator double() {
                Uc *pu = reinterpret_cast<Uc*>(p);
                Ur n = (pu[7] & 0xFFULL) | ((pu[6] & 0xFFULL) << 8)
                        | ((pu[5] & 0xFFULL) << 16) | ((pu[4] & 0xFFULL) << 24)
                        | ((pu[3] & 0xFFULL) << 32) | ((pu[2] & 0xFFULL) << 40)
                        | ((pu[1] & 0xFFULL) << 48) | ((pu[0] & 0xFFULL) << 56);
                return *reinterpret_cast<double*>(&n);
        }
        inline BEDoubleRef &operator=(const double &d) {
                Uc *pc = reinterpret_cast<Uc*>(p);
                const Ur *pu = reinterpret_cast<const Ur*>(&d);
                pc[0] = (*pu >> 56) & 0xFFu;
                pc[1] = (*pu >> 48) & 0xFFu;
                pc[2] = (*pu >> 40) & 0xFFu;
                pc[3] = (*pu >> 32) & 0xFFu;
                pc[4] = (*pu >> 24) & 0xFFu;
                pc[5] = (*pu >> 16) & 0xFFu;
                pc[6] = (*pu >> 8) & 0xFFu;
                pc[7] = *pu & 0xFFu;
                return *this;
        }
        inline BEDoubleRef &operator=(const BEDoubleRef &d) {
                *p = *d.p;
                return *this;
        }
};

pour une raison vraiment étrange, le premier opérateur d'assignation n'a correctement assigné que les octets 1 à 7. Octet 0 toujours il y avait quelque non-sens en elle, qui a cassé tout comme il y a un signe bit et une partie de l'ordre.

j'ai essayé d'utiliser les syndicats comme une solution:

union {
    double d;
    Uc c[8];
} un;
Uc *pc = un.c;
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
*p = un.d;

mais ça n'a pas marché non plus. En fait, c'était un peu mieux - il seulement échoué pour les nombres négatifs.

à ce stade, je pense à ajouter un test simple pour l'endianness native, puis tout faire via char* pointeurs avec if (LITTLE_ENDIAN) contrôles autour. Faire pire encore, le programme fait un usage intensif des syndicats tout autour, ce qui semble fonctionner bien pour l'instant, mais après tout ce désordre Je ne serai pas surpris si elle casse soudainement pour aucune raison apparente.

3
répondu Sergei Tachenov 2011-10-27 12:23:34

vous avez vu ça ? Quelle est la stricte règle d'alias?

le lien contient un lien secondaire à cet article avec des exemples de gcc. http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

essayer une union comme celle-ci serait plus proche du problème.

union a_union {
    int i;
    double *d;
};

de cette façon vous avez 2 types, un int et un double* pointant vers le même mémoire. Dans ce cas, l'utilisation du double (*(double*)&i) pourrait causer le problème.

2
répondu Cobusve 2017-05-23 12:18:07

Voici la mienne: En pense que c'est un bug dans tous les GCC v5.x et plus tard

#include <iostream>
#include <complex>
#include <pmmintrin.h>

template <class Scalar_type, class Vector_type>
class simd {
 public:
  typedef Vector_type vector_type;
  typedef Scalar_type scalar_type;
  typedef union conv_t_union {
    Vector_type v;
    Scalar_type s[sizeof(Vector_type) / sizeof(Scalar_type)];
    conv_t_union(){};
  } conv_t;

  static inline constexpr int Nsimd(void) {
    return sizeof(Vector_type) / sizeof(Scalar_type);
  }

  Vector_type v;

  template <class functor>
  friend inline simd SimdApply(const functor &func, const simd &v) {
    simd ret;
    simd::conv_t conv;

    conv.v = v.v;
    for (int i = 0; i < simd::Nsimd(); i++) {
      conv.s[i] = func(conv.s[i]);
    }
    ret.v = conv.v;
    return ret;
  }

};

template <class scalar>
struct RealFunctor {
  scalar operator()(const scalar &a) const {
    return std::real(a);
  }
};

template <class S, class V>
inline simd<S, V> real(const simd<S, V> &r) {
  return SimdApply(RealFunctor<S>(), r);
}



typedef simd<std::complex<double>, __m128d> vcomplexd;

int main(int argc, char **argv)
{
  vcomplexd a,b;
  a.v=_mm_set_pd(2.0,1.0);
  b = real(a);

  vcomplexd::conv_t conv;
  conv.v = b.v;
  for(int i=0;i<vcomplexd::Nsimd();i++){
    std::cout << conv.s[i]<<" ";
  }
  std::cout << std::endl;
}

devrait donner

c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 
c010200:~ peterboyle$ ./a.out 
(1,0) 

mais sous-O3: je pense que c'est faux et une erreur de compilateur

c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(0,0) 

sous g++4.9

c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0) 

sous llvm xcode

c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0) 
1
répondu Peter Boyle 2017-05-06 14:19:09

je ne comprends pas vraiment votre problème. Le compilateur a fait exactement ce qu'il était censé faire dans votre exemple. La conversion union est ce que vous avez fait dans f1 . Dans f2 c'est une typographie normale de pointeur, que vous l'avez moulé à un syndicat est hors de propos, c'est toujours un pointeur casting

0
répondu Patrick Schlüter 2010-06-01 17:12:52