厳密なエイリアス規則とは何ですか?
-
01-07-2019 - |
質問
について尋ねるとき C における一般的な未定義の動作, 、私が厳密なエイリアシング規則に言及したよりもさらに啓発された魂たち。
彼らは何を話している?
解決
厳密なエイリアシングの問題が発生する典型的な状況は、システムのワード サイズのバッファ (へのポインタなど) に構造体 (デバイス/ネットワーク メッセージなど) をオーバーレイする場合です。 uint32_t
または 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
] をループの実行前に 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 の一部であり、おそらく buff のコンテンツをロードする命令が含まれています。もう一度言いますが、おそらく C++ を使用していて、これはコンパイラがインライン化できると考える、テンプレート化されたヘッダーのみの実装です。あるいは、単に自分の便宜のために .c ファイルに書き込んだものかもしれません。いずれにしても、未定義の動作が引き続き発生する可能性があります。内部で何が起こっているのかをある程度知っていたとしても、それは依然としてルール違反であるため、明確に定義された動作は保証されません。したがって、単語区切りのバッファーを受け取る関数をラップするだけでは必ずしも役に立ちません。
では、これを回避するにはどうすればよいでしょうか?
ユニオンを使用します。ほとんどのコンパイラは、厳密なエイリアシングについて問題を起こすことなくこれをサポートしています。これは C99 で許可されており、C11 では明示的に許可されています。
union { Msg msg; unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)]; };
コンパイラで厳密なエイリアシングを無効にすることができます (f[no-]strict-aliasing gcc で))
使用できます
char*
システムの単語の代わりにエイリアスを作成します。ルールでは例外が認められていますchar*
(含むsigned char
そしてunsigned char
)。常に想定されているのは、char*
他のタイプに別名を付けます。ただし、これは他の方法では機能しません。構造体が文字のバッファーにエイリアスを付けるという想定はありません。
初心者は注意してください
これは、2 つのタイプを互いに重ね合わせるときに発生する可能性のある地雷原の 1 つだけです。についても学ぶ必要があります エンディアンネス, 単語の配置, 、および調整の問題に対処する方法 構造体のパッキング 正しく。
脚注
1 C 2011 6.5 7 で左辺値がアクセスできる型は次のとおりです。
- オブジェクトの有効な型と互換性のある型、
- オブジェクトの有効な型と互換性のある型の修飾されたバージョン、
- オブジェクトの有効な型に対応する符号付きまたは符号なしの型、
- オブジェクトの有効な型の修飾されたバージョンに対応する符号付き型または符号なし型である型、
- メンバー内に前述の型の 1 つを含む集合体または共用体型 (再帰的に、サブ集合体または含まれる共用体のメンバーを含む)、または
- キャラクタータイプ。
他のヒント
私が見つけた最良の説明はマイク・アクトンによるものです。 厳密なエイリアシングについて. 。PS3 の開発に少し焦点を当てていますが、基本的には単なる GCC です。
記事より:
「厳密なエイリアシングとは、異なる型のオブジェクトへのポインタを参照解除しても同じメモリ位置を決して参照しないという C (または C++) コンパイラによる仮定です (つまり、お互いにエイリアスを使います。)
基本的に、あなたが持っている場合、 int*
を含む何らかの記憶を指している int
そして、あなたは float*
その記憶を、 float
あなたはルールを破ります。コードがこれを尊重しない場合、コンパイラのオプティマイザによってコードが破損する可能性が高くなります。
ルールの例外は、 char*
, 、任意の型を指すことができます。
これは、セクション 3.10 に記載されている厳密なエイリアシング ルールです。 C++03 標準(他の回答は適切な説明を提供しますが、ルール自体を提供するものはありません):
プログラムが、次のいずれかの型以外の左辺値を介してオブジェクトの格納値にアクセスしようとした場合、動作は未定義です。
- オブジェクトの動的タイプ、
- オブジェクトの動的タイプの cv 修飾バージョン、
- オブジェクトの動的型に対応する符号付き型または符号なし型の型、
- オブジェクトの動的型の cv 修飾バージョンに対応する符号付き型または符号なし型である型、
- メンバー内に前述の型の 1 つを含む集合体または共用体型 (再帰的に、部分集合体または含まれる共用体のメンバーを含む)
- オブジェクトの動的型の (おそらく cv 修飾された) 基本クラス型である型、
- ある
char
またはunsigned char
タイプ。
C++11 そして C++14 文言 (変更点を強調):
プログラムがオブジェクトの保存された値にアクセスしようとすると、 グローバル値 次のタイプのいずれか以外の場合、動作は未定義です。
- オブジェクトの動的タイプ、
- オブジェクトの動的タイプの cv 修飾バージョン、
- オブジェクトの動的型に類似した型 (4.4 で定義)、
- オブジェクトの動的型に対応する符号付き型または符号なし型の型、
- オブジェクトの動的型の cv 修飾バージョンに対応する符号付き型または符号なし型である型、
- 前述の型のいずれかをその中に含む集合体または共用体型 要素または非静的データメンバー (再帰的に、 要素または非静的データメンバー 部分集合または含まれる結合の)、
- オブジェクトの動的型の (おそらく cv 修飾された) 基本クラス型である型、
- ある
char
またはunsigned char
タイプ。
小さな変更が 2 つあります。 グローバル値 の代わりに 左辺値, 、および集合体/共用体の場合の明確化。
3 番目の変更では、より強力な保証が行われます (強力なエイリアシング ルールが緩和されます)。新しいコンセプトは、 似たようなタイプ エイリアスを付けても安全になりました。
また、 C 文言 (C99;ISO/IEC 9899:1999 6.5/7;まったく同じ文言が ISO/IEC 9899:2011 §6.5 ¶7) で使用されています。
オブジェクトは、次のタイプのいずれかを持つLValue式の式によってのみアクセスされる保存値を持つものとします 73) または 88):
- オブジェクトの有効な型と互換性のある型、
- オブジェクトの効果的なタイプと互換性のあるタイプの適格バージョン、
- オブジェクトの有効なタイプに対応する署名型または符号なしタイプであるタイプ、
- オブジェクトの効果的なタイプの適格バージョンに対応する署名型または署名されていないタイプであるタイプ、
- メンバー間の前述のタイプの1つ(再帰的に、サブアグレージ酸組合のメンバーを含む)を含む集約または組合タイプ、または
- キャラクタータイプ。
73) または 88) このリストの目的は、オブジェクトがエイリアス化される場合とされない場合がある状況を指定することです。
注記
これは私のものからの抜粋です 「厳密なエイリアシング ルールとは何ですか?なぜ気にする必要があるのですか?」 書き上げる。
厳密なエイリアシングとは何ですか?
C および C++ では、エイリアシングは、格納された値にアクセスできる式のタイプに関係します。C と C++ の両方で、標準では、どの式のタイプがどのタイプのエイリアスとして使用できるかを指定しています。コンパイラとオプティマイザは、エイリアス規則に厳密に従っていることを前提とすることが許可されているため、この用語は 厳密なエイリアス規則. 。許可されていない型を使用して値にアクセスしようとすると、次のように分類されます。 未定義の動作(UB)。未定義の動作が発生すると、すべての賭けが外れ、プログラムの結果は信頼できなくなります。
残念ながら、厳密なエイリアシング違反では、多くの場合、期待した結果が得られますが、新しい最適化を備えた将来のバージョンのコンパイラによって、有効だと思われていたコードが壊れる可能性が残ります。これは望ましくないことであり、厳密なエイリアス規則とその違反を避ける方法を理解することは価値のある目標です。
なぜ気にするのかをさらに理解するために、厳密なエイリアシング ルールに違反した場合に発生する問題と、型語呂合わせで使用される一般的な手法が厳密なエイリアシング ルールに違反することが多いため、型語呂合わせについて説明します。また、語呂合わせを正しく入力する方法についても説明します。
予備的な例
いくつかの例を見てみましょう。その後、標準の内容を正確に説明し、さらにいくつかの例を調べて、厳密なエイリアシングを回避し、見逃した違反を見つける方法を見てみましょう。以下は驚くべきことではない例です (ライブの例):
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
私たちには、 int* が占有しているメモリを指す 整数 これは有効なエイリアスです。オプティマイザは、代入が ip によって占有されている値を更新できます バツ.
次の例は、未定義の動作を引き起こすエイリアシングを示しています (ライブの例):
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?
}
関数内で ふー 私たちは int* そして 浮く*, 、この例では、 ふー そして、両方のパラメータが同じメモリの場所を指すように設定します。この例では、 整数. 。注意してください、 再解釈_キャスト 式をテンプレート パラメータで指定された型を持つかのように扱うようにコンパイラに指示しています。この場合、式を処理するように指示しています。 &バツ まるでタイプがあるかのように 浮く*. 。私たちは素朴に 2 番目の結果を期待するかもしれません コート することが 0 ただし、使用して最適化を有効にすると、 -O2 gcc と Clang は両方とも次の結果を生成します。
0
1
これは予期されないかもしれませんが、未定義の動作を呼び出しているため、完全に有効です。あ 浮く 有効な別名を付けることができません 整数 物体。したがって、オプティマイザは次のことを想定できます。 定数1 逆参照時に保存される 私 ストアスルー以降の戻り値になります f に有効な影響を与えることができませんでした 整数 物体。Compiler Explorer にコードを接続すると、まさにこれが起こっていることがわかります(ライブの例):
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
オプティマイザを使用する タイプベースのエイリアス分析 (TBAA) 仮定します 1 が返され、定数値がレジスタに直接移動されます。 イーエックス 戻り値を運びます。TBAA は、ロードとストアを最適化するためにエイリアスを許可する型に関する言語ルールを使用します。この場合、TBAA は次のことを知っています。 浮く エイリアスを付けることはできません 整数 の負荷を最適化します。 私.
さて、ルールブックへ
この規格では、具体的に何が許可され、何が禁止されているのでしょうか?標準言語は単純ではないため、各項目について、意味を示すコード例を提供していきます。
C11 規格には何と記載されていますか?
の 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/clang には拡張子があります そして また 割り当てが可能になります 符号なし整数* に 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.
C++17 ドラフト標準の内容
C++17 ドラフト標準のセクション [basic.lval] 段落 11 言います:
プログラムが、次のいずれかの型以外の 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
}
注目に値する 符号付き文字 は上記のリストには含まれていませんが、これは との顕著な違いです。 C それは言う キャラクタータイプ.
タイプパニングとは何ですか
ここまで来て、なぜエイリアスを使用する必要があるのかと疑問に思うかもしれません。通常、答えは次のとおりです。 タイプダジャレ, 、多くの場合、使用されるメソッドは厳密なエイリアス規則に違反します。
場合によっては、型システムを回避して、オブジェクトを別の型として解釈したいことがあります。これはと呼ばれます 語呂合わせ, 、メモリのセグメントを別のタイプとして再解釈します。 語呂合わせ これは、表示、転送、または操作するためにオブジェクトの基礎となる表現にアクセスする必要があるタスクに役立ちます。型パニングが使用されていることが判明した典型的な領域は、コンパイラ、シリアル化、ネットワーク コードなどです。
従来、これはオブジェクトのアドレスを取得し、それを再解釈したい型のポインタにキャストして、その値にアクセスすることによって、つまりエイリアシングによって実現されてきました。例えば:
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 と同じサイズです ダブル:
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);
//...
十分な最適化レベルでは、まともな最新のコンパイラーは前述のコードと同一のコードを生成します。 再解釈_キャスト 方法または 連合 の方法 語呂合わせ. 。生成されたコードを調べると、 register mov (ライブ コンパイラ エクスプローラーの例).
C++20 と bit_cast
C++20 では、次のことが得られる可能性があります。 ビットキャスト (提案からのリンクで実装が利用可能) これは、constexpr コンテキストで使用できるだけでなく、type-pun の簡単かつ安全な方法を提供します。
以下は使用方法の例です ビットキャスト ダジャレを入力するには 符号なし整数 に 浮く, (ライブで見てください):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
場合 に そして から 型のサイズが同じではないため、中間の struct15 を使用する必要があります。を含む構造体を使用します。 sizeof( unsigned int ) 文字配列 (4バイトの符号なし整数を想定します) になる から タイプと 符号なし整数 として に タイプ。:
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 ;
}
この中間型が必要なのは残念ですが、それが現在の制約です。 ビットキャスト.
厳密なエイリアシング違反の捕捉
C++ では厳密なエイリアスを捕捉するための優れたツールはあまりありませんが、私たちが持っているツールは、厳密なエイリアス違反の一部のケースと、不整列のロードとストアの一部のケースを捕捉します。
gcc はフラグを使用します -fstrict-エイリアシング そして -Wstrict エイリアシング 偽陽性/偽陰性がないわけではありませんが、いくつかのケースを検出できます。たとえば、次の場合、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 です。これらは直接的な厳密なエイリアシング違反ではありませんが、厳密なエイリアシング違反の一般的な結果です。たとえば、次の場合は、clang を使用してビルドするとランタイム エラーが発生します。 -fsanitize=アドレス
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 は両方とも、次を使用して C スタイルのキャストの診断を生成します。 -Wold-style-cast. 。これにより、未定義の型の語呂合わせで reinterpret_cast の使用が強制されます。一般に、reinterpret_cast はより詳細なコード レビューのためのフラグである必要があります。コード ベースで reinterpret_cast を検索して監査を実行することも簡単です。
C については、すべてのツールがすでにカバーされており、C 言語の大規模なサブセットのプログラムを徹底的に分析する静的アナライザーである tis-interpreter もあります。前述の例の C バージョンを考えると、 -fstrict-エイリアシング 1 つのケースを見逃します (ライブで見てください)
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.
最後にあります タイサン 現在開発中です。このサニタイザーは、シャドウ メモリ セグメントに型チェック情報を追加し、アクセスをチェックしてエイリアシング ルールに違反していないかどうかを確認します。このツールは潜在的にすべてのエイリアス違反を捕捉できるはずですが、実行時に大きなオーバーヘッドが発生する可能性があります。
厳密なエイリアシングはポインタだけを参照するのではなく、参照にも影響します。私はこれについてブースト開発者 Wiki に論文を書き、非常に好評だったので、私のコンサルティング Web サイトのページにしました。それが何であるか、なぜ人々をこれほど混乱させるのか、そしてそれに対して何をすべきかについて完全に説明しています。 厳密なエイリアシングに関するホワイトペーパー. 。特に、C++ にとって共用体が危険な動作である理由、および C と C++ の両方で移植可能な唯一の修正が memcpy の使用である理由を説明します。これがお役に立てば幸いです。
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 バージョンでは) これは「厳密なエイリアシングの問題」を出力します。これは、コンパイラーが「check」関数の「h」が「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 条件はアセンブラ コードから完全に削除されます。
語呂合わせ (ユニオンを使用するのではなく) ポインター キャストを使用することは、厳密なエイリアスを破る主な例です。
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」型の共用体メンバーを読み取るアクションが実装定義の動作を呼び出すため、プログラムが未定義の動作を呼び出すと記載されています。このような推論はナンセンスですが、元の問題に何も対処せずに言語を不必要に複雑にする効果的なタイプのルールの基礎を形成します。
元の問題を解決する最良の方法は、おそらく規則の目的について脚注を扱い、それが規範的であるかのように扱い、実際にエイリアスを使用して矛盾するアクセスを伴う場合を除き、ルールを執行不能にすることです。次のようなものを考えると:
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
型の左辺値で行われます 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
実行中のその時点で別の参照が存在するため、マークされた行にあります 同じストレージにアクセスするために使用されます.
Had Defect Report 028 によると、元の例では 2 つのポインターの作成と使用が重複しているために UB が呼び出されており、「有効なタイプ」などの複雑さを追加する必要がなく、物事がより明確になっていたはずです。
多くの回答を読んだ後、何かを追加する必要があると感じました。
厳密なエイリアシング (これについては後で説明します) 重要なので:
メモリアクセスは (パフォーマンス的に) コストがかかる場合があります。 データはCPUレジスタで操作されます 物理メモリに書き戻される前に。
2 つの異なる CPU レジスタのデータが同じメモリ空間に書き込まれる場合、 どのデータが「生き残る」かは予測できません Cでコーディングするとき。
アセンブリでは、CPU レジスタのロードとアンロードを手動でコーディングするため、どのデータがそのまま残っているかがわかります。しかし、C は (ありがたいことに) この詳細を抽象化しています。
2 つのポインタがメモリ内の同じ場所を指す可能性があるため、次のような結果が生じる可能性があります。 衝突の可能性を処理する複雑なコード.
この追加のコードは処理が遅く、 パフォーマンスに悪影響を及ぼす 追加のメモリ読み取り/書き込み操作が実行されるため、速度が遅くなり、(おそらく) 不必要になるためです。
の 厳密なエイリアス規則により、冗長なマシンコードを回避できます 場合によっては あるべきです 2 つのポインタが同じメモリ ブロックを指していないと想定しても問題ありません ( restrict
キーワード)。
厳密なエイリアシングでは、異なる型へのポインターがメモリ内の異なる場所を指していると想定しても安全であると述べています。
2 つのポインターが異なる型 (たとえば、 int *
そして 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) {...}
ここで、厳密なエイリアス規則を満たすことにより、ステップ 3 を回避でき、コードの実行が大幅に高速化されます。
実際、これを追加することで、 restrict
キーワードを使用すると、関数全体を次のように最適化できます。
負荷
a
そしてb
記憶から。追加
a
にb
.結果を両方に保存します
a
そしてへb
.
この最適化は、衝突の可能性があるため、以前は実行できませんでした (ここで、 a
そして b
2 倍ではなく 3 倍になります)。
厳密なエイリアシングでは、同じデータへの異なるポインター型は許可されません。
この記事 問題を詳細に理解するのに役立つはずです。
技術的には、C++ では、厳密なエイリアス規則はおそらく適用されません。
間接の定義に注意してください (* 演算子):
単項 * 演算子は間接演算を実行します。それが適用される式は、オブジェクトタイプへのポインター、または関数タイプへのポインターでなければなりません 結果はオブジェクトを参照する左辺値です または機能 式が指すもの.
こちらからも glvalue の定義
glvalueは、その評価がオブジェクトのアイデンティティを決定する式です(... snip)
したがって、明確に定義されたプログラム トレースでは、glvalue はオブジェクトを参照します。 したがって、いわゆる厳密なエイリアス規則は決して適用されません。 これはデザイナーが望んでいたものではないかもしれません。