C での一般的な未定義の振る舞いについて尋ねると、厳密なエイリアシング規則を参照するよりも魂の方が啓発されていました。
彼らは何を話している?
あなたがあなたのシステムのWordサイズのバッファ(uint32_t
sやuint16_t
sへのポインタのような)の上に構造体(デバイス/ネットワークmsgのような)をオーバーレイするとき、あなたが厳しいエイリアス問題に遭遇する典型的な状況はあります。そのようなバッファ上に構造体をオーバーレイする場合、またはポインタキャストによってそのような構造体上にバッファをオーバーレイする場合、厳密なエイリアシング規則に容易に違反する可能性があります。
したがって、この種の設定で、メッセージを何かに送信したい場合は、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パラグラフ71 未定義の動作です。残念ながら、このようにコーディングすることはできますが、おそらく警告が表示されます。コードを正常にコンパイルするようにしてください。
(GCCはエイリアシング警告を出す能力がやや矛盾しているように見え、時には私たちにわかりやすい警告を出し、時にはそうしないようにします。)
なぜこの振る舞いが未定義であるかを見るために、厳密なエイリアシング規則がコンパイラを購入するものについて考える必要があります。基本的に、この規則では、ループを実行するたびにbuff
の内容を更新するための命令を挿入することを考える必要はありません。代わりに、エイリアシングについての厄介で強制的でない仮定で最適化するとき、それはそれらの命令を省略し、ループが実行される前に一度CPUレジスタにbuff[0]
とbuff[1
]をロードし、そしてループの本体をスピードアップします。厳密なエイリアシングが導入される前は、コンパイラはbuff
の内容がいつでもどこからでも誰かによって変更される可能性があるというパラノイアの状態にある必要がありました。そのため、Edgeのパフォーマンスをさらに向上させるために、ほとんどの人がポインタを型打ちしないことを前提として、厳密なエイリアシング規則が導入されました。
あなたが例が工夫されていると思うなら、あなたが代わりに持っているならば、あなたがあなたの代わりに送信をしている他の関数にバッファを渡しているならば、これは起こるかもしれません。
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ファイルに書いたものに過ぎないかもしれません。とにかく未定義の振る舞いがまだ起こるかもしれません。フードの下で何が起こっているのかを知っていても、それはまだルール違反であるため、明確に定義された動作は保証されていません。そのため、Wordで区切られたバッファを受け取る関数をラップするだけでは必ずしも役に立ちません。
では、どうすればいいですか?
共用体を使用してください。ほとんどのコンパイラは、厳密なエイリアシングについて文句を言わずにこれをサポートします。これはC99で許可され、C11で明示的に許可されています。
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
コンパイラで厳密なエイリアシングを無効にすることができます(gccの f [no-] strict-aliasing ))
あなたのシステムのWordの代わりにchar*
をエイリアスに使うことができます。規則はchar*
の例外を許可します(signed char
とunsigned char
を含む)。 char*
は他の型をエイリアスすると常に仮定されています。しかしこれは他の方法ではうまくいきません:あなたの構造体がcharのバッファをエイリアスするという仮定はありません。
初心者には注意してください
これは、2つのタイプを重ね合わせるときの唯一の潜在的な地雷原です。また、 エンディアン 、 単語の配置 、および対処方法についても学ぶ必要があります。 構造体 を正しくパッキングすることによるアライメントの問題。
1 C 2011 6.5 7が左辺値にアクセスを許可するタイプは次のとおりです。
私が見つけた最良の説明はMike Actonによるものです。 厳密なエイリアシングの理解 。 PS3の開発に少し焦点を当てていますが、それは基本的にはGCCだけです。
記事から:
「厳密なエイリアシングは、C(またはC++)コンパイラによって作成された、異なるタイプのオブジェクトへのポインタの参照解除が同じメモリロケーションを参照することは決してないという前提です(つまり、エイリアス)。
そのため、基本的にint
を含むメモリを指すint*
があり、それからそのメモリを指すfloat*
を指定し、それをfloat
として使用すると、ルールを破ることになります。あなたのコードがこれを尊重しなければ、コンパイラのオプティマイザはあなたのコードを壊すでしょう。
この規則の例外はchar*
で、これは任意の型を指すことができます。
これはC++ 03規格のセクション3.10にある厳密なエイリアシング規則です(他の回答では十分な説明が得られますが、規則自体は提供されません) :
プログラムが以下のタイプのうちの1つ以外の左辺値を通してオブジェクトの記憶された値にアクセスしようとするならば、振る舞いは未定義です:
- オブジェクトの動的型
- 動的型のオブジェクトのcv修飾版
- オブジェクトの動的型に対応する符号付きまたは符号なし型である型
- オブジェクトの動的型のcv修飾バージョンに対応する符号付きまたは符号なし型である型
- そのメンバーの中に前述のタイプの1つを含む集合または共用タイプ(再帰的に、サブ集合または包含共用体のメンバーを含む)
- オブジェクトの動的型の(おそらくcv修飾可能な)基本クラス型である型
char
型またはunsigned char
型。
C++ 11およびC++ 14の表現(変更箇所を強調) :
プログラムが次の型のうちの1つ以外の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) このリストの目的は、オブジェクトに別名が付けられているかどうかを特定することです。
厳密なエイリアシングはポインタだけを指すのではなく、参照にも影響を与えます。ブースト開発者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バージョンでは)これは "strict aliasing problem"を出力します。なぜならコンパイラは "check"関数の "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条件はアセンブラコードから完全になくなりました。
(キャストを使うのではなく)ポインタキャストによる型打ち は、厳密なエイリアシングを破る主な例です。
多くの答えを読んだ後、何かを追加する必要があると感じています。
厳密なエイリアシング(少し説明します)重要な理由:
メモリアクセスは(パフォーマンスに関して)高価になる可能性があるため、物理メモリに書き戻す前にデータはCPUレジスタで操作されますになります。
2つの異なるCPUレジスタのデータが同じメモリ空間に書き込まれる場合、Cでコーディングすると、どのデータが「生き残る」かは予測できません.
CPUレジスタのロードとアンロードを手動でコーディングするアセンブリでは、どのデータがそのまま残っているかがわかります。しかし、Cは(ありがたいことに)この詳細を抽象化します。
2つのポインターはメモリ内の同じ場所を指すことができるため、結果として衝突の可能性を処理する複雑なコードになります。
この余分なコードは低速であり、パフォーマンスを低下させます低速で(おそらく)不要な追加のメモリ読み取り/書き込み操作を実行するためです。
厳密なエイリアスルールにより、冗長なマシンコードを回避できます 2つのポインターが同じものを指していないと想定しても安全ですメモリブロック(restrict
キーワードも参照)。
Strictエイリアスは、異なる型へのポインターがメモリ内の異なる場所を指していると仮定しても安全だと述べています。
コンパイラは、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倍になります)。
厳密なエイリアシングでは、同じデータに対して異なるポインタ型を使用できません。
この記事 は問題を詳細に理解するのに役立ちます。