厳密なエイリアシング規則とは何ですか?

厳密なエイリアシングの問題が発生する典型的な状況は、構造体 (デバイス/ネットワーク メッセージなど) をシステムのワード サイズのバッファー (uint32_t へのポインターなど) にオーバーレイする場合です。 s または uint16_t s)。構造体をそのようなバッファーにオーバーレイしたり、ポインターのキャストによってそのような構造体にバッファーをオーバーレイしたりすると、厳密なエイリアシング ルールに簡単に違反する可能性があります。

したがって、この種のセットアップでは、何かにメッセージを送信したい場合、同じメモリ チャンクを指す 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]);   
    }
}

厳密なエイリアシング規則により、このセットアップは違法になります。互換性のある型ではないオブジェクト、または C 2011 6.5 パラグラフ 7 1 で許可されている他の型のいずれかではないオブジェクトをエイリアスするポインターの逆参照 未定義の動作です。残念ながら、まだこの方法でコーディングできます。おそらく いくつかの警告が表示され、問題なくコンパイルできますが、コードを実行すると奇妙な予期しない動作が発生します。

(GCC は、エイリアシング警告を出す能力に多少の一貫性がないように見えます。わかりやすい警告を出す場合と出さない場合があります。)

この動作が定義されていない理由を理解するには、厳密なエイリアシング規則がコンパイラに与える影響について考える必要があります。基本的にこのルールではbuffの内容を更新する命令を挿入することは考えなくていい ループのすべての実行。代わりに、最適化の際に、エイリアシングに関する強制されていない面倒な仮定を使用して、それらの命令を省略し、buff[0] をロードすることができます。 と buff[1] ループが実行される前に一度 CPU レジスタに書き込み、ループの本体を高速化します。厳密なエイリアシングが導入される前は、コンパイラは buff の内容が 前のメモリストアによって変更される可能性があります。そのため、パフォーマンス上の優位性をさらに高めるために、またほとんどの人がポインターをタイプダジャレしないと仮定して、厳密なエイリアシング規則が導入されました。

この例が不自然だと思われる場合は、代わりに送信を行う別の関数にバッファを渡している場合に、これが発生する可能性があることに注意してください.

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

そして、この便利な機能を利用するために以前のループを書き直しました

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

コンパイラは、SendMessage をインライン化しようとする能力がある場合とそうでない場合があり、buff を再度ロードするかしないかを決定する場合としない場合があります。 SendMessage の場合 個別にコンパイルされた別の API の一部であり、おそらくバフのコンテンツをロードするための指示が含まれています。繰り返しになりますが、C++ を使用している可能性があります。これは、コンパイラがインライン化できると考える、テンプレート化されたヘッダーのみの実装です。あるいは、自分の便宜のために .c ファイルに書き込んだだけかもしれません。とにかく、未定義の動作が引き続き発生する可能性があります。内部で何が起こっているかをある程度知っていても、それは規則違反であるため、明確に定義された動作は保証されません。したがって、単語で区切られたバッファを受け取る関数をラップするだけでは、必ずしも役に立ちません。

どうすればこれを回避できますか?

    <リ>

    ユニオンを使用してください。ほとんどのコンパイラは、厳密なエイリアシングについて文句を言うことなくこれをサポートしています。これは C99 で許可され、C11 で明示的に許可されています。

      union {
          Msg msg;
          unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
      };
    
    <リ>

    コンパイラで厳密なエイリアシングを無効にすることができます (gcc では f[no-]strict-aliasing))

    <リ>

    char* を使用できます システムの言葉の代わりにエイリアシングを行います。規則では char* の例外が許可されています (signed char を含む) および unsigned char )。 char* であると常に仮定されます 他のタイプの別名。ただし、これは別の方法では機能しません。構造体が文字のバッファーにエイリアスを設定するという前提はありません。

初心者は注意

これは、2 つのタイプを互いに重ね合わせた場合に発生する可能性のある地雷原の 1 つにすぎません。また、エンディアン、ワード アラインメント、および構造体を正しくパッキングすることでアラインメントの問題に対処する方法についても学ぶ必要があります。

脚注

1 C 2011 6.5 7 で左辺値がアクセスできる型は次のとおりです:

  • オブジェクトの有効な型と互換性のある型
  • オブジェクトの有効な型と互換性のある型の修飾バージョン
  • オブジェクトの有効な型に対応する符号付きまたは符号なしの型
  • オブジェクトの有効な型の修飾されたバージョンに対応する符号付きまたは符号なしの型である型
  • メンバーの中に前述の型のいずれかを含む集約型または共用体型 (再帰的に、部分集約型または含まれる共用体のメンバーを含む)、または
  • 文字タイプ。

私が見つけた最良の説明は、Mike Acton による、Strict Aliasing の理解です。 PS3 開発に少し焦点を当てていますが、それは基本的に GCC だけです。

記事より:

つまり、基本的に int* がある場合 int を含むメモリを指している そして float* を指します そのメモリに移動し、float として使用します あなたはルールを破ります。コードがこれを尊重しない場合、コンパイラのオプティマイザがコードを壊す可能性が高くなります。

ルールの例外は char* です 、これは任意の型を指すことができます。


これは、C++03 のセクション 3.10 にある厳密なエイリアシング ルールです。 標準(他の回答は良い説明を提供しますが、ルール自体を提供するものはありません):

C++11 および C++14 文言 (変更を強調):

2 つの小さな変更:glvalue lvalue の代わりに 、および集約/結合ケースの明確化。

3 番目の変更は、より強力な保証を行います (強力なエイリアシング ルールを緩和します):similar types の新しい概念 エイリアスしても安全です。

また、C 文言 (C99; ISO/IEC 9899:1999 6.5/7; ISO/IEC 9899:2011 §6.5 ¶7 でもまったく同じ文言が使用されています):