Cでの一般的な未定義動作について尋ねるとき、人々は厳密なエイリアシングルールを参照することがあります。
彼らは何を話している?
11 に答える
厳密なエイリアシングの問題が発生する一般的な状況は、構造体(デバイス/ネットワークメッセージなど)をシステムのワードサイズのバッファー(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]);
}
}
厳密なエイリアシングルールにより、この設定は不正になります。互換性のあるタイプまたはC20116.5段落71で許可されている他のタイプのオブジェクトをエイリアシングするポインターの間接参照は未定義の動作です。残念ながら、この方法でコーディングすることはできますが、警告が表示されたり、正常にコンパイルされたりする場合がありますが、コードを実行すると予期しない動作が発生します。
(GCCは、エイリアシング警告を出す能力に多少一貫性がないように見えます。友好的な警告を与えることもあれば、そうでないこともあります。)
この動作が定義されていない理由を確認するには、厳密なエイリアシングルールがコンパイラを購入するものについて考える必要があります。buff
基本的に、このルールを使用すると、ループのすべての実行の内容を更新するための命令の挿入について考える必要はありません。代わりに、エイリアシングに関するいくつかの厄介な強制されていない仮定を使用して最適化する場合、ループが実行される前に一度それらの命令を省略し、CPUレジスタにロードbuff[0]
してロードし、ループの本体を高速化できます。厳密なエイリアシングが導入される前は、コンパイラは、先行するメモリストアによってbuff[1]
内容が変更される可能性があるパラノイアの状態で動作する必要がありました。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をインライン化しようとするのに十分な能力がある場合とできない場合があり、バフを再度ロードするかどうかを決定する場合としない場合があります。SendMessage
が個別にコンパイルされた別のAPIの一部である場合、おそらくbuffのコンテンツをロードするための命令が含まれています。繰り返しになりますが、おそらく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 20116.57で左辺値へのアクセスを許可するタイプは次のとおりです。
- オブジェクトの有効なタイプと互換性のあるタイプ、
- オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
- オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型、
- オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型、
- メンバーの中に前述のタイプの1つを含む集合体または共用体タイプ(再帰的に、サブ集合体または含まれる共用体のメンバーを含む)、または
- 文字タイプ。
私が見つけた最も良い説明は、Mike Acton、UnderstandingStrictAliasingによるものです。PS3の開発に少し焦点を当てていますが、それは基本的にはGCCだけです。
記事から:
「厳密なエイリアシングは、C(またはC ++)コンパイラによって行われた、異なるタイプのオブジェクトへのポインタの間接参照が同じメモリ位置を参照することは決してない(つまり、互いにエイリアスする)という仮定です。」
したがって、基本的に、をint*
含むメモリを指している場合、そのメモリを指しint
、それをルール違反float*
として使用します。float
コードがこれを尊重しない場合、コンパイラのオプティマイザがコードを破壊する可能性があります。
ルールの例外は、char*
であり、これは任意のタイプを指すことができます。
これは、 C ++ 03標準のセクション3.10にある厳密なエイリアシングルールです(他の回答は適切な説明を提供しますが、ルール自体を提供するものはありません)。
プログラムが次のタイプのいずれか以外の左辺値を介してオブジェクトの格納された値にアクセスしようとした場合、動作は未定義です。
- オブジェクトの動的タイプ、
- オブジェクトの動的タイプのcv修飾バージョン。
- オブジェクトの動的型に対応する符号付きまたは符号なしの型である型、
- オブジェクトの動的型のcv修飾バージョンに対応する符号付きまたは符号なし型である型。
- メンバーの中に前述のタイプの1つを含む集合体または共用体タイプ(再帰的に、サブ集合体または含まれる共用体のメンバーを含む)、
- オブジェクトの動的型の(おそらくcv修飾された)基本クラス型である型、
char
またはunsigned char
タイプ。
C++11およびC++14の文言(変更を強調):
プログラムが次のタイプのいずれか以外のglvalueを介してオブジェクトの保存された値にアクセスしようとした場合、動作は未定義です。
- オブジェクトの動的タイプ、
- オブジェクトの動的タイプのcv修飾バージョン。
- オブジェクトの動的タイプに類似したタイプ(4.4で定義)、
- オブジェクトの動的型に対応する符号付きまたは符号なしの型である型、
- オブジェクトの動的型のcv修飾バージョンに対応する符号付きまたは符号なし型である型。
- 要素または非静的データメンバー(再帰的に、サブアグリゲートまたは含まれるユニオンの要素または非静的データメンバーを含む)の中に前述のタイプの1つを含む集合体または共用体タイプ。
- オブジェクトの動的型の(おそらくcv修飾された)基本クラス型である型、
char
またはunsigned char
タイプ。
2つの変更は小さかった:左辺値の代わりにglvalue、および集計/結合の場合の明確化。
3番目の変更により、より強力な保証が行われます(強力なエイリアシングルールが緩和されます)。同様のタイプの新しい概念で、エイリアシングが安全になりました。
また、Cの文言(C99; ISO / IEC 9899:1999 6.5/7;まったく同じ文言がISO/IEC 9899:2011§6.5¶7で使用されています):
オブジェクトは、次のタイプのいずれかを持つ左辺値式によってのみアクセスされる格納された値を持つ必要があります73)または88):
- オブジェクトの有効なタイプと互換性のあるタイプ、
- オブジェクトの有効なタイプと互換性のあるタイプの認定バージョン、
- オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型、
- オブジェクトの有効な型の修飾されたバージョンに対応する符号付きまたは符号なしの型である型、
- メンバーの中に前述のタイプの1つを含む集合体または共用体タイプ(再帰的に、サブ集合体または含まれる共用体のメンバーを含む)、または
- 文字タイプ。
73)または88)このリストの目的は、オブジェクトがエイリアス化される場合とされない場合がある状況を指定することです。
厳密なエイリアシングはポインターだけを参照するのではなく、参照にも影響を与えます。Boost 開発者の wiki でそれについての論文を書きましたが、非常に評判が良かったので、私のコンサルティング Web サイトのページに変えました。それが何であるか、なぜ人々をそれほど混乱させるのか、そしてそれについて何をすべきかを完全に説明しています. 厳密なエイリアシングに関するホワイト ペーパー。特に、共用体が C++ にとって危険な動作である理由と、memcpy を使用することが C と C++ の両方で移植可能な唯一の修正方法である理由を説明しています。これが役に立てば幸いです。
Doug T. がすでに書いたことの補足として、おそらく 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;
}
でコンパイルしgcc -O2 -o check check.c
ます。通常 (私が試したほとんどの gcc バージョンで)、これは「厳密なエイリアシングの問題」を出力します。これは、コンパイラが「h」を「check」関数の「k」と同じアドレスにすることはできないと想定しているためです。そのため、コンパイラはif (*h == 5)
アウェイを最適化し、常に printf を呼び出します。
興味のある方は、gcc 4.6.3 によって生成され、x64 用の ubuntu 12.04.2 で実行される x64 アセンブラー コードを以下に示します。
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
そのため、if 条件はアセンブラー コードから完全に削除されています。
(ユニオンを使用するのではなく)ポインターキャストを介した型のパンニングは、厳密なエイリアシングを破る主な例です。
厳密なエイリアシングでは、同じデータへの異なるポインタタイプは許可されていません。
この記事は、問題を完全に理解するのに役立つはずです。