Cでの一般的な未定義の動作について尋ねるとき、人々は厳密なエイリアシング規則を参照することがあります。
彼らは何を話している?
Cでの一般的な未定義の動作について尋ねるとき、人々は厳密なエイリアシング規則を参照することがあります。
彼らは何を話している?
回答:
厳密なエイリアシングの問題が発生する一般的な状況は、(デバイスまたはネットワークメッセージのような)構造体をシステムのワードサイズのバッファ(uint32_t
sまたはuint16_t
s へのポインタのような)にオーバーレイするときです。このようなバッファーに構造体をオーバーレイするか、ポインターのキャストを介してそのような構造体にバッファーをオーバーレイすると、厳密なエイリアス規則に簡単に違反する可能性があります。
したがって、この種の設定で何かにメッセージを送信したい場合、同じメモリのチャンクを指す2つの互換性のないポインタが必要になります。それから私はこのようなものを単純にコーディングするかもしれません(を備えたシステム上でsizeof(int) == 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で許可されている他の型の1つであるオブジェクトをエイリアスするポインターの逆参照は、未定義の動作です。残念ながら、あなたはまだ、この方法をコーディングすることができます多分あなたは、コードを実行したときにのみ、奇妙な予期しない動作を持っている、それは罰金をコンパイルする必要があり、いくつかの警告を取得します。
(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をインライン化しようとすることができるか、または十分にスマートであるとは限りません。また、buffを再度ロードするかしないかを決定する場合としない場合があります。SendMessage
個別にコンパイルされた別のAPIの一部である場合、おそらくbuffのコンテンツをロードするための指示があります。次に、C ++を使用している可能性があります。これは、テンプレート化されたヘッダーのみの実装であり、コンパイラーはインライン化できると考えています。あるいは、自分の便宜のために.cファイルに書き込んだものかもしれません。とにかく、未定義の動作が引き続き発生する可能性があります。内部で何が起こっているかを知っていても、それは依然としてルール違反であるため、明確に定義された動作は保証されません。したがって、単語区切りバッファを使用する関数をラップするだけでは、必ずしも効果はありません。
どうすればこれを回避できますか?
ユニオンを使用します。ほとんどのコンパイラは、厳密なエイリアシングについて文句を言うことなくこれをサポートします。これはC99では許可されており、C11では明示的に許可されています。
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
コンパイラで厳格なエイリアスを無効にすることができます(f [no-] strict-aliasing in gcc))
char*
システムの単語の代わりにエイリアスに使用できます。ルールはchar*
(signed char
およびを含むunsigned char
)の例外を許可します。は常にchar*
他のタイプのエイリアスと見なされます。ただし、これは他の方法では機能しません。構造体が文字のバッファをエイリアスするという仮定はありません。
初心者は注意してください
これは、2つのタイプを互いにオーバーレイする場合の1つの潜在的な地雷原です。また、エンディアン、単語の整列、構造体を正しくパッキングして整列の問題に対処する方法についても学習する必要があります。
1 C 2011 6.5 7が左辺値にアクセスを許可するタイプは次のとおりです。
unsigned char*
遠方で使用される可能性はありますchar*
か?バイトが署名されておらず、署名された動作の奇妙さ(特にオーバーフローが発生しない)を望まないため、の基になる型としてではunsigned char
なく使用する傾向がありますchar
byte
unsigned char *
も問題ありません。
uint32_t* buff = malloc(sizeof(Msg));
とそれに続くユニオンunsigned int asBuffer[sizeof(Msg)];
バッファ宣言は異なるサイズを持ち、どちらも正しくありません。malloc
コールは、それが必要以上に4倍大きくなります...私はそれを明確にするためであることを理解ボンネットの下に4バイトアライメント(それをしない)と労働組合に依存するが、それはバグ私をなし・ザされます少ない...
私が見つけた最高の説明は、Mike Acton、Understanding Strict Aliasingです。PS3開発に少し焦点を当てていますが、それは基本的には単なるGCCです。
記事から:
「厳密なエイリアシングは、C(またはC ++)コンパイラによって作成された、異なる型のオブジェクトへのポインタの逆参照は、同じメモリ位置を参照しない(つまり、相互にエイリアシングする)ことを前提としています。」
だから、基本的にあなたが持っている場合はint*
、いくつかのメモリへのポインティングを含むint
、その後、あなたはポイントfloat*
そのメモリとしてそれを使用するfloat
ルールを破ります。コードがこれを尊重しない場合、コンパイラのオプティマイザがコードを破壊する可能性が高くなります。
ルールの例外は、char*
任意のタイプを指すことができるです。
これは、C ++ 03標準のセクション3.10にある厳密なエイリアスルールです(他の答えは良い説明を提供しますが、ルール自体は提供していません)。
プログラムが次のタイプのいずれか以外の左辺値を介してオブジェクトの格納された値にアクセスしようとした場合の動作は未定義です。
- オブジェクトの動的タイプ
- オブジェクトの動的タイプのcv修飾バージョン、
- オブジェクトの動的型に対応する符号付きまたは符号なしの型である型
- オブジェクトの動的タイプのcv修飾バージョンに対応する符号付きまたは符号なしタイプのタイプ
- メンバーの中に前述のタイプの1つを含む集合体またはユニオンタイプ(再帰的に、サブアグリゲートまたは含まれるユニオンのメンバーを含む)、
- オブジェクトの動的な型の(場合によってはcv修飾された)基本クラス型である型
- A
char
またはunsigned char
タイプ。
C ++ 11およびC ++ 14の表現(変更を強調):
プログラムが次のタイプのいずれか以外のglvalueを介してオブジェクトの格納された値にアクセスしようとした場合の動作は未定義です。
- オブジェクトの動的タイプ
- オブジェクトの動的タイプのcv修飾バージョン、
- オブジェクトの動的タイプに類似したタイプ(4.4で定義)
- オブジェクトの動的型に対応する符号付きまたは符号なしの型である型
- オブジェクトの動的タイプのcv修飾バージョンに対応する符号付きまたは符号なしタイプのタイプ
- そのうち、前述のタイプのうちの1つを含む凝集体または共用タイプの要素または非静的データメンバ(を含むが、再帰的に要素又は非静的データメンバ subaggregateまたは含ま連合)、
- オブジェクトの動的な型の(場合によってはcv修飾された)基本クラス型である型
- A
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)このリストの目的は、オブジェクトがエイリアスされる場合とされない場合がある状況を指定することです。
wow(&u->s1,&u->s2)
することが合法である場合、ポインターを使用して変更する場合でも合法である必要があり、これによりu
、エイリアシングルールは、容易にするために設計されました。
これは私の「厳格なエイリアシングルールとは何か、なぜ私たちは気にするのか」からの抜粋です。書き上げる。
CおよびC ++では、エイリアシングは、格納された値へのアクセスを許可されている式のタイプに関係しています。CとC ++の両方で、標準では、どの型にエイリアスを付けることができる式の型が指定されています。コンパイラとオプティマイザは、エイリアシングルールに厳密に従っていることを前提としているため、厳密なエイリアシングルールと呼ばれます。許可されていないタイプを使用して値にアクセスしようとすると、未定義の動作(UB)として分類されます。未定義の動作が発生すると、すべての賭けが無効になり、プログラムの結果は信頼できなくなります。
残念ながら、厳密なエイリアシング違反があると、予想どおりの結果が得られることが多く、新しい最適化を備えたコンパイラの将来のバージョンでは、有効であると考えたコードが壊れる可能性があります。これは望ましくないことであり、厳密なエイリアシングルールとその違反を回避する方法を理解することは価値のある目標です。
なぜ私たちが気にするのかをさらに理解するために、厳密なエイリアシングルールに違反するときに発生する問題について説明します。
いくつかの例を見てみましょう。次に、規格の発言について正確に話し、さらにいくつかの例を調べて、厳密なエイリアシングを回避し、見逃した違反をキャッチする方法を確認します。これは驚くべきことではない例です(実例):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
我々は持っているのint *メモリを指すが、によって占めint型とこれが有効なエイリアスです。オプティマイザは、ipによる割り当てがxが占める値を更新できると想定する必要があります。
次の例は、未定義の動作につながるエイリアスを示しています(実際の例):
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
関数fooでは、int *とfloat *を受け取ります。この例では、fooを呼び出し、両方のパラメーターを、この例ではintを含む同じメモリー位置を指すように設定します。reinterpret_castは、テンプレートパラメーターで指定された型があるかのように式を扱うようにコンパイラーに指示していることに注意してください。この場合、式&xをfloat *型であるかのように扱うように指示しています。単純に2番目のcoutの結果が0になると予想する場合がありますが、-O2を使用して最適化を有効にすると、gccとclangの両方が次の結果を生成します。
0
1
これは予想されないかもしれませんが、未定義の動作を呼び出したので完全に有効です。フロートは有効にエイリアスできないint型のオブジェクト。したがって、オプティマイザは、iを逆参照するときに格納された定数1を戻り値と見なすことができます。これは、fを介した格納がintオブジェクトに有効に影響しなかったためです。コンパイラエクスプローラーでコードをプラグインすると、これがまさに何が起こっているかを示しています(実例):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
Type-Based Alias Analysis(TBAA)を使用するオプティマイザは、1が返されると想定し、定数値をレジスタeaxに直接移動します。このレジスタは、戻り値を保持します。TBAAは、ロードとストアを最適化するために、どのタイプにエイリアスを許可するかに関する言語規則を使用します。この場合、TBAAは、floatがエイリアスおよびintできないことを認識し、iの負荷を最適化します。
私たちが許可されていることと許可されていないことを規格は正確に何と言っていますか?標準言語は単純ではないので、各項目について、意味を示すコード例を提供しようと思います。
C11の標準は、セクションに次のように述べている6.5式パラグラフ7:
オブジェクトは、次のタイプのいずれかを持つ左辺値式によってのみアクセスされる格納された値を持つものとします。88) —オブジェクトの有効なタイプと互換性のあるタイプ。
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
—オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
—オブジェクトの有効なタイプに対応する符号付きまたは符号なしタイプのタイプ
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
GCC /打ち鳴らすには、拡張子があるとも割り当てることができますunsigned int型を*に* int型を、彼らは互換性のある型でなくても。
—オブジェクトの有効な型の修飾バージョンに対応する署名付きまたは署名なしの型である型
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
—メンバーの中に前述のタイプの1つを含む集約またはユニオンタイプ(再帰的に、サブアグリゲートまたは含まれるユニオンのメンバーを含む)、または
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
—文字タイプ。
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
セクション[basic.lval]パラグラフ11のC ++ 17ドラフト標準は次のように述べています。
:次のタイプの他の数の挙動が未定義であるのglvalueを通じてオブジェクトの格納された値にアクセスするためのプログラムしようとした場合63 -オブジェクトの動的な型、(11.1)
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2)—オブジェクトの動的タイプのcv修飾バージョン、
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3)—オブジェクトの動的タイプに類似したタイプ(7.5で定義)
(11.4)—オブジェクトの動的タイプに対応する符号付きまたは符号なしタイプのタイプ
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5)—オブジェクトの動的タイプのcv修飾バージョンに対応する署名付きまたは署名なしのタイプであるタイプ
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6)—要素または非静的データメンバー(再帰的に、サブアグリゲートまたは含まれるユニオンの要素または非静的データメンバーを含む)の中に前述のタイプの1つを含む集約またはユニオンタイプ、
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7)—オブジェクトの動的タイプの(おそらくcv修飾された)基本クラスタイプであるタイプ
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8)— char、unsigned char、またはstd :: byteタイプ。
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
上記のリストには、signed charに注意する価値はありません。これは、文字タイプを表すCとの大きな違いです。
ここまで来て、疑問に思うかもしれませんが、なぜ別名を付けたいのでしょうか?答えは通常、punと入力することです。多くの場合、使用されるメソッドは厳密なエイリアス規則に違反しています。
型システムを回避して、オブジェクトを別の型として解釈したい場合があります。これはタイプパニングと呼ばれ、メモリのセグメントを別のタイプとして再解釈します。タイプパニングは、オブジェクトの基になる表現にアクセスして、表示、転送、または操作する必要があるタスクに役立ちます。タイプパニングが使用されているのがわかる典型的な領域は、コンパイラ、シリアル化、ネットワークコードなどです。
伝統的に、これはオブジェクトのアドレスを取得し、それを再解釈したい型のポインターにキャストしてから値にアクセスすることによって、つまり別名を付けることによって実現されていました。例えば:
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
これまで見てきたように、これは有効なエイリアスではないため、未定義の動作を呼び出しています。しかし、伝統的にコンパイラーは厳密なエイリアシング規則を利用せず、このタイプのコードは通常は機能するだけでしたが、開発者は残念ながらこの方法で慣れてきました。タイプパニングの一般的な代替方法は、Cで有効であるが、C ++では未定義の動作である共用体を使用する方法です(実際の例を参照)。
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
これはC ++では無効であり、一部のユーザーは、共用体の目的はバリアント型を実装するためだけであると考えており、型のパンニングに共用体を使用することは不正行為だと感じています。
CとC ++の両方で型パンニングを行うための標準的な方法はmemcpyです。これは少し重いように見えるかもしれませんが、オプティマイザは型パンニングのためのmemcpyの使用を認識し、それを最適化して、レジスタの移動を生成する必要があります。たとえば、int64_tがdoubleと同じサイズであることがわかっている場合:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
memcpyを使用できます。
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
十分な最適化レベルでは、適切な最新のコンパイラは、前述のreinterpret_castメソッドまたは型パンニングのunionメソッドと同じコードを生成します。生成されたコードを調べると、register mov(ライブコンパイラエクスプローラーの例)のみを使用していることがわかります。
C ++ 20では、bit_cast(提案からのリンクで利用可能な実装)を得ることができます。これは、タイプパンするためのシンプルで安全な方法を提供するだけでなく、constexprコンテキストで使用可能です。
以下は、bit_castを使用して、unsigned intをfloatに入力する方法の例です(実際に表示されます)。
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
場合へとより型が同じサイズを持っていない、それは中間struct15を使用するために私たちを必要とします。我々は含む構造体に使用するのsizeof(unsigned int型)文字配列を(想定している4バイトの符号なし整数をする)から、タイプとunsigned int型としては、するにはタイプ:
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
この中間型が必要なのは残念ですが、それが現在のbit_castの制約です。
C ++で厳密なエイリアシングをキャッチするための優れたツールは多くありません。厳密なエイリアシング違反のいくつかのケースや、整列されていないロードとストアのいくつかのケースをキャッチするツールがあります。
フラグ-fstrict -aliasingと-Wstrict-aliasingを使用するgccは、誤検知 /誤検知がないわけではありませんが、いくつかのケースをキャッチできます。たとえば、次の場合はgccで警告が生成されます(ライブで確認してください)。
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
この追加のケースをキャッチしません(ライブでご覧ください):
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
clangはこれらのフラグを許可しますが、実際には警告を実装していないようです。
私たちが利用できるもう1つのツールはASanで、これは整列されていないロードとストアをキャッチできます。これらは直接の厳密なエイリアシング違反ではありませんが、厳密なエイリアシング違反の一般的な結果です。たとえば、次の場合、-fsanitize = addressを使用してclangでビルドするとランタイムエラーが発生します
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
私がお勧めする最後のツールはC ++固有のものであり、厳密にはツールではありませんが、コーディングの実践であり、Cスタイルのキャストは許可されていません。gccとclangはどちらも、-Wold-style-castを使用してCスタイルのキャストの診断を生成します。これにより、未定義の型パンがreinterpret_castを使用するように強制されます。一般に、reinterpret_castは、より詳細なコードレビューのためのフラグである必要があります。また、コードベースでreinterpret_castを検索して監査を実行する方が簡単です。
Cの場合は、すべてのツールがすでにカバーされています。また、C言語の大部分のプログラムを徹底的に分析する静的アナライザーであるtis-interpreterもあります。-fstrict -aliasingを使用すると1つのケースが見落とされる、前の例のCバージョンが指定されている場合(ライブで表示)
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeterは3つすべてをキャッチできます。次の例では、tis-kernalをtis-interpreterとして呼び出します(出力は簡潔にするために編集されています)。
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
最後に、現在開発中のTySanがあります。このサニタイザーは、シャドウメモリセグメントに型チェック情報を追加し、アクセスをチェックして、エイリアスルールに違反していないかどうかを確認します。このツールは、すべてのエイリアス違反をキャッチできる可能性がありますが、実行時のオーバーヘッドが大きくなる可能性があります。
reinterpret_cast
ができるか、何をcout
意味するのかわからない私のような人々にとって、従うのは困難です。(C ++について言及しても問題ありませんが、元の質問はCおよびIIUCに関するものでした。これらの例は、Cでも同様に正しく記述できます。)
厳密なエイリアシングはポインターだけを参照するのではなく、参照にも影響します。ブースト開発者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」を「k」と同じアドレスにすることはできないためです。そのため、コンパイラーはif (*h == 5)
アウェイを最適化し、常にprintfを呼び出します。
ここで興味のある人のために、x64用のubuntu 12.04.2で実行されているgcc 4.6.3によって生成されたx64アセンブラコードがあります:
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
したがって、if条件はアセンブラーコードから完全になくなります。
long long*
とint64_t
*に当てはまる一部のシステムでは)。正常なコンパイラはa long long*
を認識し、int64_t*
それらが同じように格納されている場合は同じストレージにアクセスできると期待するかもしれませんが、そのような扱いはもはや流行ではありません。
(ユニオンを使用するのではなく)ポインターキャストによる型パンニングは、厳密なエイリアシングを解除する主な例です。
fpsync()
fpとしての書き込みとintとしての読み取りまたはその逆のディレクティブを実行した場合にのみ確実に機能するように実装を指定することもできます(個別の整数およびFPUパイプラインとキャッシュを使用する実装で) 、そのようなディレクティブは高価になる可能性がありますが、コンパイラーがすべてのユニオンアクセスでそのような同期を実行するほど高価ではありません]。または、実装は、共通の初期シーケンスを使用する状況を除いて、結果の値が使用できないことを指定できます。
C89の理論的根拠によれば、標準の作成者は、コンパイラーに次のようなコードが与えられることを要求したくありませんでした:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
を指すx
可能性を可能にするために、割り当てとreturnステートメントの間の値を再ロードする必要があり、結果としてへの割り当てがの値を変更する可能性があります。コンパイラーが、上記のような状況ではエイリアシングが発生しないことを前提とする権利を与えられるべきであるという考えは、議論の余地がないものでした。p
x
*p
x
残念ながら、C89の作成者は、文字どおりに読んだ場合、次の関数でさえ未定義の動作を呼び出すようにルールを記述しました。
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
それは種類の左辺値を使用しているためint
、アクセスするタイプのオブジェクトをstruct S
、およびint
アクセスに使用することができる種類の中ではありませんstruct S
。構造体と共用体の非文字型メンバーのすべての使用を未定義の動作として扱うのはばかげているので、ほとんどの人は、ある型の左辺値が別の型のオブジェクトにアクセスするために使用される可能性がある少なくともいくつかの状況があることを認識しています。残念ながら、C標準委員会はそれらの状況が何であるかを定義することに失敗しました。
問題の多くは、次のようなプログラムの動作について尋ねた不具合レポート#028の結果です。
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
欠陥レポート#28は、タイプ「double」の共用体メンバーを書き込み、タイプ「int」の1つを読み取るアクションが実装定義の動作を呼び出すため、プログラムが未定義の動作を呼び出すと述べています。そのような推論は無意味ですが、元の問題に対処するために何もせずに言語を不必要に複雑にする効果的な型ルールの基礎を形成します。
元の問題を解決する最良の方法は、ルールの目的に関する脚注を規範的であるかのように扱い、実際にエイリアスを使用したアクセスの競合が関係する場合を除いて、ルールを強制不可能にすることでしょう。次のようなものが与えられた:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
をinc_int
介し*p
てアクセスされるストレージへのすべてのアクセスはtypeの左辺値で行われるため、内部での競合はありません。またint
、test
からp
目に見えるように派生struct S
し、次回s
使用されるまで、そのストレージへのすべてのアクセスが行われるため、競合は発生しません。p
がすでに起こっているでしょう。
コードが少し変更された場合...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
ここで、p
とs.x
マークされた行へのアクセスの間にエイリアスの競合があります。実行のその時点で、同じストレージへのアクセスに使用される別の参照が存在するためです。
欠陥レポート028によると、2つのポインターの作成と使用が重複しているため、元の例ではUBが呼び出されたため、「Effective Types」やその他の複雑な要素を追加しなくても、より明確になります。
答えの多くを読んだ後、何かを追加する必要があると感じます。
厳密なエイリアス(少し説明します)は重要です。
メモリアクセスはコストがかかる(パフォーマンスに関して)可能性があるため、物理メモリに書き戻す前にCPUレジスタでデータを操作します。
2つの異なるCPUレジスタのデータが同じメモリ空間に書き込まれる場合、Cでコーディングするときにどのデータが「存続」するかを予測できません。
CPUレジスタのロードとアンロードを手動でコーディングするアセンブリでは、どのデータがそのまま残っているかがわかります。しかし、Cは(ありがたいことに)この詳細を抽象化します。
2つのポインタがメモリ内の同じ場所を指すことができるため、これにより、衝突の可能性を処理する複雑なコードが生成される可能性があります。
この余分なコードは遅く、(おそらく)不要な余分なメモリの読み取り/書き込み操作を実行するため、パフォーマンスが低下します。
厳格なエイリアシング規則は、私たちは冗長なマシンコードを避けることができ、いる場合でなければなりません(も参照の二つのポインタが同じメモリブロックを指していないと仮定しても安全restrict
キーワードを)。
Strictエイリアシングは、異なる型へのポインタがメモリ内の異なる場所を指していると想定することが安全であると述べています。
コンパイラが2つのポインタが異なる型(たとえば、int *
とa float *
)を指していることに気づいた場合、メモリアドレスが異なると見なされ、メモリアドレスの衝突から保護されないため、マシンコードが高速になります。
例:
次の関数を想定します。
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
a == b
(両方のポインターが同じメモリを指す)場合に対処するために、メモリからCPUレジスタにデータをロードする方法を順序付けてテストする必要があるため、コードは次のようになります。
ロードa
およびb
メモリから。
に追加a
しb
ます。
保存 b
して再読み込みし a
ます。
(CPUレジスタからメモリに保存し、メモリからCPUレジスタにロードします)。
に追加b
しa
ます。
a
(CPUレジスタから)メモリに保存します。
手順3は物理メモリにアクセスする必要があるため、非常に低速です。ただし、同じメモリアドレスa
をb
指すインスタンスから保護する必要があります。
厳密なエイリアシングにより、これらのメモリアドレスが明確に異なることをコンパイラに通知することで、これを防ぐことができます(この場合、ポインタがメモリアドレスを共有している場合は実行できないさらなる最適化が可能になります)。
これは、異なるタイプを使用してポイントすることにより、2つの方法でコンパイラーに伝えることができます。つまり:
void merge_two_numbers(int *a, long *b) {...}
restrict
キーワードを使用します。すなわち:
void merge_two_ints(int * restrict a, int * restrict b) {...}
ここで、Strict Aliasingルールを満たすことにより、ステップ3を回避でき、コードの実行速度が大幅に向上します。
実際、restrict
キーワードを追加することで、関数全体を次のように最適化できます。
ロードa
およびb
メモリから。
に追加a
しb
ます。
a
との両方に結果を保存しb
ます。
この最適化可能性があるため、衝突(ここで、以前に行われていることができませんでしたa
し、b
三倍の代わりに倍増することでしょう)。
b
(再読み込みではなく)と再読み込みのみを行いますa
。私はそれが今より明確であることを望みます。
restrict
が、後者の方がほとんどの状況でより効果的であり、いくつかの制約を緩めるregister
と、restrict
役に立たない場合のいくつかを埋めることができると思います。標準を特定の証拠が存在しない場合でもコンパイラがエイリアスを推定する必要がある場所を単に説明するのではなく、コンパイラがエイリアスの証拠を認識することを期待するすべてのケースを完全に説明するように標準を扱うことが「重要」であったかどうかはわかりません。
restrict
キーワードは操作の速度だけでなくその数も最小化します。これは意味があるかもしれません...つまり、結局のところ、最速の操作はまったく操作がないことです:)
厳密なエイリアシングでは、同じデータへの異なるポインタ型は許可されていません。
この記事は、問題を完全に理解するのに役立ちます。
int
を含む構造体int
)。
技術的にはC ++では、厳密なエイリアシングルールはおそらく適用されません。
間接指定(*演算子)の定義に注意してください。
単項*演算子は間接参照を実行します。適用される式は、オブジェクト型へのポインター、または関数型へのポインターであり、結果は、式が指すオブジェクトまたは関数を参照する左辺値です。
また、glvalueの定義から
glvalueは、その評価がオブジェクトのアイデンティティを決定する式です(... snip)
したがって、明確に定義されたプログラムトレースでは、glvalueはオブジェクトを参照します。したがって、いわゆる厳密なエイリアシングルールは適用されません。これは、設計者が望んでいたものとは異なる場合があります。
int foo;
、左辺値式は何にアクセスし*(char*)&foo
ますか?それはタイプのオブジェクトchar
ですか?そのオブジェクトは同時に存在しfoo
ますか?foo
前述のタイプのオブジェクトの格納された値を変更するために書き込みchar
ますか?もしそうならchar
、タイプの左辺値を使用してタイプのオブジェクトの格納された値にアクセスできるようにするルールはありますint
か?
int i;
は各文字型in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
*(char *)&i`およびの4つのオブジェクトを作成しi
ますか?最後に、volatile
「オブジェクト」の定義を満たさないハードウェアレジスタにアクセスするための-qualifiedポインタを許可する標準はありません。
c
ていc++faq
ます。