38

伝えるべき恐ろしい話はありますか?GCC マニュアルは最近、-fstrict-aliasing と共用体を介したポインターのキャストに関する警告を追加しました。

[...]アドレスを取得し、結果のポインターをキャストし、結果を逆参照すると、キャストがユニオン型を使用している場合でも、未定義の動作が発生します[強調が追加されました]。

    union a_union {
        int i;
        double d;
    };

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

この未定義の動作を説明する例はありますか?

この質問は、C99標準が何を言っているか、何を言っていないかに関するものではないことに注意してください。今日のgccやその他の既存のコンパイラの実際の機能についてです。

推測にすぎませんが、1 つの潜在的な問題はdto 3.0 の設定にある可能性があります。d直接読み取られることはなく、「ある程度互換性のある」ポインターを介して読み取られることもない一時変数であるため、コンパイラーはわざわざそれを設定しない場合があります。そして、f() はスタックからガベージを返します。

私の単純で素朴な試みは失敗します。例えば:

#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("%d\n", f1());
    printf("%d\n", f2());
    return 0;
}

CYGWIN で問題なく動作します:

-2147483648
-2147483648

アセンブラを見ると、gccが完全に最適化tして いることがわかりますf1()。事前に計算された答えを単純に保存します。

movl    $-2147483648, %eax

whilef2()は 3333333.0 を浮動小数点スタックにプッシュし、戻り値を抽出します。

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

また、関数もインライン化されています (これが微妙な厳密なエイリアシングのバグの原因になっているようです) が、ここでは関係ありません。(そして、このアセンブラーはそれほど関連性はありませんが、裏付けとなる詳細を追加します。)

また、アドレスを取得することは明らかに間違っていることに注意してください (または、未定義の動作を説明しようとしている場合は正しいです)。たとえば、私たちが知っているように、これは間違っています。

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

同様に、これが間違っていることはわかっています。

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

これに関する背景説明については、例を参照してください。

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
( = Google でページを検索し、キャッシュされたページを表示)

厳密なエイリアシング規則とは何ですか?
C++ での C99 の厳密なエイリアシング規則 (GCC)

最初のリンク、7 か月前の ISO 会議の議事録草案では、ある参加者がセクション 4.16 で次のように述べています。

ルールが十分に明確であると考える人はいますか? それらを本当に解釈できる人は誰もいません。

その他の注意事項:私のテストは gcc 4.3.4 で、-O2 を使用しました。オプション -O2 および -O3 は、-fstrict-aliasing を意味します。GCC マニュアルの例では、sizeof(double) >= sizeof(int);を想定しています。それらが不平等であっても構いません。

また、cellperformace リンクで Mike Acton が指摘しているように、-Wstrict-aliasing=2ではなく 、ここの例で=3は生成されます。warning: dereferencing type-punned pointer might break strict-aliasing rules

4

7 に答える 7

13

GCC がユニオンについて警告しているという事実は、必ずしもユニオンが現在機能していないことを意味するわけではありません。しかし、これはあなたのものよりも少し単純な例です:

#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);
}

出力:

$ 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

したがって、GCC 4.3.4 では、共用体は「その日を節約」します (出力「-3」が必要であると仮定します)。厳密なエイリアシングに依存し、2 番目のケース (のみ) で出力 "3" をもたらす最適化を無効にします。-Wall を指定すると、USE_UNION はタイプしゃれの警告も無効にします。

テストする gcc 4.4 はありませんが、このコードを試してみてください。実際のコードはd、ユニオンを読み戻す前にメモリが初期化されているかどうかをテストします。私のコードは、メモリが変更されているかどうかをテストします。

ところで、double の半分を int として安全に読み取る方法は次のとおりです。

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

GCC で最適化すると、次のようになります。

    int thing() {
401130:       55                      push   %ebp
401131:       89 e5                   mov    %esp,%ebp
401133:       83 ec 10                sub    $0x10,%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

そのため、memcpy への実際の呼び出しはありません。これを行っていない場合、ユニオンキャストが GCC で機能しなくなった場合に得られるものに値します ;-)

于 2010-06-02T15:17:01.343 に答える
5

ちょっとネクロ投稿ですが、ホラーストーリーです。ネイティブのバイトオーダーがビッグエンディアンであることを前提に書かれたプログラムを移植しています。今、私はリトルエンディアンでも動作する必要があります。残念ながら、データにはさまざまな方法でアクセスできるため、どこでもネイティブ バイト オーダーをそのまま使用することはできません。たとえば、64 ビット整数は、2 つの 32 ビット整数として、または 4 つの 16 ビット整数として、あるいは 16 個の 4 ビット整数としても扱うことができます。さらに悪いことに、ソフトウェアはある種のバイトコードのインタープリターであり、データはそのバイトコードによって形成されるため、メモリに正確に格納されているものを把握する方法はありません。たとえば、バイトコードには、16 ビット整数の配列を書き込み、それらのペアに 32 ビット float としてアクセスする命令が含まれている場合があります。そして、それを予測したり、バイトコードを変更したりする方法はありません。

したがって、ネイティブのエンディアンに関係なく、ビッグ エンディアンの順序で格納された値を操作するための一連のラッパー クラスを作成する必要がありました。最適化なしで、Linux 上の Visual Studio および GCC で完全に機能しました。しかし、gcc -O2 を使用すると、地獄が解き放たれました。多くのデバッグの後、理由がここにあることがわかりました:

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

このコードは、32 ビット整数に格納された float の 32 ビット表現を double に変換するために使用されました。コンパイラは、D への代入後に *pF への代入を行うことを決定したようです。その結果、コードが初めて実行されたとき、D の値はガベージであり、結果の値は 1 回の繰り返しで「遅く」なりました。

奇跡的に、その時点で他の問題はありませんでした。そこで、ネイティブ ビッグ エンディアン オーダーの RISC プロセッサ上の HP-UX という元のプラットフォームで新しいコードをテストすることにしました。今度は私の新しいクラスで再び壊れました:

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;
        }
};

いくつかの非常に奇妙な理由で、最初の代入演算子はバイト 1 から 7 までしか正しく割り当てられませんでした。バイト 0 には常にナンセンスがあり、符号ビットと順序の一部があるため、すべてが壊れていました。

回避策としてユニオンを使用しようとしました:

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;

しかし、それもうまくいきませんでした。実際、それは少し良くなりました - 負の数に対してのみ失敗しました。

char*この時点で、ネイティブ エンディアンの簡単なテストを追加し、ポインタを介してすべてをif (LITTLE_ENDIAN)チェックすることを考えています。さらに悪いことに、このプログラムはあちこちで共用体を多用しており、今のところ問題なく動作しているように見えますが、この混乱の後で明らかな理由もなく突然壊れても驚かないでしょう。

于 2011-10-27T12:23:34.180 に答える
4

次のコードが「間違っている」というあなたの主張:

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

... 間違っている。2 つの共用体メンバーのアドレスを取得して外部関数に渡すだけでは、未定義の動作にはなりません。これらのポインターの 1 つを無効な方法で逆参照することによってのみ得られます。たとえば、関数 foo が、渡したポインターを逆参照せずにすぐに戻る場合、動作は未定義ではありません。C99 標準を厳密に読むと、未定義の動作を呼び出さずにポインターを逆参照できる場合もあります。たとえば、2 番目のポインターによって参照される値を読み取り、最初のポインターを介して値を格納することができますが、両方とも動的に割り当てられたオブジェクト (つまり、「宣言された型」を持たないオブジェクト) を指している場合に限ります。

于 2010-07-11T13:25:00.777 に答える
3

エイリアシングは、コンパイラが同じメモリ部分への 2 つの異なるポインターを持っている場合に発生します。ポインターを型キャストすることにより、新しい一時ポインターを生成しています。たとえば、オプティマイザーがアセンブリ命令を並べ替える場合、2 つのポインターにアクセスすると、2 つのまったく異なる結果が生じる可能性があります。つまり、同じアドレスへの書き込みの前に読み取りを並べ替える可能性があります。これが未定義の動作である理由です。

非常に単純なテスト コードで問題が発生することはほとんどありませんが、多くのことが行われている場合に発生します。

警告は、労働組合が特殊なケースではないことを明確にすることだと思います。

エイリアシングの詳細については、次の Wikipedia の記事を参照してください: http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

于 2010-05-25T16:33:41.073 に答える
2

これを見たことがありますか ? 厳密なエイリアシング規則とは何ですか?

このリンクには、gcc の例を含むこの記事への二次リンクが含まれています。http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

このような組合を試みると、問題に近づきます。

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

そうすれば、同じメモリを指す int と double* の 2 つの型があります。この場合、double を使用する(*(double*)&i)と問題が発生する可能性があります。

于 2010-06-01T16:04:32.417 に答える
1

これが私のものです:これはすべてのGCC v5.x以降のバグだと思います

#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;
}

与えるべき

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

しかし、-O3 の下では: これは間違っていて、コンパイラ エラーだと思います

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

g++4.9以下

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

llvm xcodeの下

c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0) 
于 2017-05-06T13:15:40.190 に答える
0

私はあなたの問題を本当に理解していません。コンパイラは、あなたの例で行うべきことを正確に行いました。変換は で行ったunionものですf1。それf2は通常のポインターの型キャストであり、ユニオンにキャストしたことは無関係です。それはまだポインターのキャストです

于 2010-06-01T17:12:52.950 に答える