言語標準は、その言語を使用するプログラマーと、幅広い最適化セットを使用して適度に高速なコードを生成したいコンパイラー作成者との間で、時々競合する利益の間でバランスを取ろうとします。変数をレジスタに保持することは、そのような最適化の 1 つです。プログラムのセクションで「生きている」変数の場合、コンパイラはそれらをレジスタに割り当てようとします。ポインターのアドレスに格納すると、プログラムのアドレス空間のどこにでも格納できます。これにより、レジスター内のすべての変数が無効になります。コンパイラーはプログラムを分析して、ポインターが指し示すことができる場所とできない場所を把握することがありますが、C (および C++) 言語標準では、これは過度の負担と見なされており、「システム」タイプのプログラムでは、多くの場合不可能な作業です。そのため、言語標準は、特定の構造が「未定義の動作」につながることを指定することで制約を緩和しているため、コンパイラの作成者はそれらが発生しないと想定し、その仮定の下でより良いコードを生成できます。の場合strict aliasing
到達した妥協点は、1 つのポインター型を使用してメモリに格納する場合、別の型の変数は変更されていないと見なされるため、レジスタに保持するか、これらの他の型への格納とロードを並べ替えることができるということです。ポインターストア。
この種の最適化の例は、このホワイト ペーパー「未定義の動作: コードに何が起こったのか?」で説明されています。
http://pdos.csail.mit.edu/papers/ub:apsys12.pdf
Linux カーネルで厳密なエイリアス規則に違反する例があります。明らかに、カーネルは最適化のために厳密なエイリアス規則を使用しないようにコンパイラに指示することで問題を回避します。「Linux カーネルは -fno-strict を使用します。 -厳密なエイリアシングに基づく最適化を無効にするエイリアシング。"
struct iw_event {
uint16_t len; /* Real length of this stuff */
...
};
static inline char * iwe_stream_add_event(
char * stream, /* Stream of events */
char * ends, /* End of stream */
struct iw_event *iwe, /* Payload */
int event_len ) /* Size of payload */
{
/* Check if it's possible */
if (likely((stream + event_len) < ends)) {
iwe->len = event_len;
memcpy(stream, (char *) iwe, event_len);
stream += event_len;
}
return stream;
}
図 7: Linux カーネルの include/net/iw_handler.h にある厳密なエイリアシング違反。GCC を使用して-fno-strict-aliasing
並べ替えの可能性を防いでいます。
2.6 型パンされたポインター逆参照
C はプログラマーに、ある型のポインターを別の型にキャストする自由を与えます。ポインター キャストは、特定のオブジェクトを別の型で再解釈するために悪用されることがよくあります。これは、型パニングと呼ばれるトリックです。そうすることで、プログラマーは、異なる型の 2 つのポインターが同じメモリー位置を指していると想定します (つまり、エイリアシング)。ただし、C 標準にはエイリアシングに関する厳密な規則があります。特に、いくつかの例外を除いて、異なる型の 2 つのポインターは別名になりません [19, 6.5]。厳密なエイリアシングに違反すると、未定義の動作が発生します。図 7 は、Linux カーネルの例を示しています。この関数は、最初に iwe->len を更新し、次に memcpy を使用して、更新された iwe->len を含む iwe の内容をバッファー ストリームにコピーします。Linux カーネルは、独自の最適化された memcpy 実装を提供することに注意してください。この場合、
iwe->len = 8;
*(int *)stream = *(int *)((char *)iwe);
*((int *)stream + 1) = *((int *)((char *)iwe) + 1);
展開されたコードは、最初に uint16_t 型の iwe->len に 8 を書き込み、次に異なる型 int を使用して iwe->len の同じメモリ位置を指す iwe を読み取ります。厳密なエイリアシング規則に従って、GCC は、異なるポインター型を使用するため、読み取りと書き込みが同じメモリ位置で発生しないと結論付け、2 つの操作を並べ替えます。したがって、生成されたコードは古い iwe->len 値をコピーします。Linux カーネルは-fno-strict-aliasing
、厳密なエイリアシングに基づく最適化を無効にするために使用します。
回答
1) このエイリアシングの場合、コンパイラはどのような最適化を実行できますか?
言語標準は、厳密に準拠するプログラムのセマンティクス (動作) について非常に具体的です。それを正しく行うには、コンパイラの作成者または言語の実装者に負担がかかります。プログラマーが一線を越えて未定義の動作を呼び出すと、これが意図したとおりに機能することを証明する責任が、コンパイラーの作成者ではなくプログラマーにあることを標準が明確に示しています。それを行う義務はありませんが、動作が呼び出されました。この時点で「何かが起こる可能性がある」と、通常はジョーク/誇張が続くと、迷惑なことに人々が言うことがあります。あなたのプログラムの場合、コンパイラは「プラットフォームに典型的な」コードを生成する可能性がありますlocalval
something
あなたが意図したようにからロードしlocalval
て に保存しDataPtr
ますが、そうする義務はないことを理解してください。localval
ストアを型の何かへのストアとuint32
見なし、ロードの逆参照を型(*(const float32*)((const void*)(&localval)))
からのロードと見なし、これらが同じ場所にないため、初期化されていないものからロードしている間を含むレジスタにある可能性があるfloat32
と結論付けますそのレジスタを予約済みの「自動」ストレージ (スタック) に「スピル」する必要があると判断した場合に予約されるスタック上の場所。ポインターを逆参照してメモリからロードする前に、メモリに格納する場合と格納しない場合があります。コードの次の内容に応じて、それが使用されず、割り当てが決定される場合があります。localval
something
localval
localval
localval
something
副作用がないため、割り当てが「デッドコード」であると判断され、レジスタへの割り当てさえ行われない場合があります。
2) 両方が同じサイズを占有するため (そうでない場合は修正してください)、そのようなコンパイラの最適化の副作用は何でしょうか?
その結果、 が指すアドレスに未定義の値が格納される可能性がありますDataPtr
。
3) 警告を安全に無視したり、エイリアシングを無効にしたりできますか?
これは、使用しているコンパイラに固有のものです-コンパイラが厳密なエイリアシング最適化をオフにする方法を文書化している場合は、コンパイラが作成する警告が何であれ、はい。
4) コンパイラが最適化を実行しておらず、最初のコンパイル後にプログラムが壊れていない場合は? コンパイラが同じように動作するたびに(最適化を行わない)、安全に想定できますか?
たぶん、プログラムの別の部分の非常に小さな変更により、コンパイラがこのコードに対して行うことが変わる可能性があります。関数が「インライン化」されている場合、コードの他の部分が混在してスローされる可能性があると考えてください。これを参照してくださいそう質問してください。
5) エイリアシングは void * タイプキャストにも適用されますか? または、標準の型キャスト (int、float など) にのみ適用されますか?
a を逆参照することはできないvoid *
ため、コンパイラは最終的なキャストの型を気にするだけです (C++ では、a を a に、またはその逆に変換すると問題が発生const
しnon-const
ます)。
6) エイリアシング ルールを無効にすると、どのような影響がありますか?
コンパイラのドキュメントを参照してください。一般に、これを行うと、より遅いコードが得られます (上記の論文の例で Linux カーネルが選択したように)。必要な関数のみを使用して、これを小さなコンパイル単位に制限します。 .
結論
あなたの質問は好奇心のためであり、これがどのように機能するか (または機能しない可能性があるか) をよりよく理解しようとしていることを理解しています。コードが移植可能であることが要件であると述べましたが、暗黙のうちに、プログラムが準拠しており、未定義の動作を呼び出さないことが要件です(そうする場合、負担がかかることを忘れないでください)。この場合、質問で指摘したように、1 つの解決策は を使用することですmemcpy
。これは、コードを準拠させて移植可能にするだけでなく、現在の gcc で可能な最も効率的な方法で意図したことを実行することが判明したためです最適化レベル-O3
コンパイラはを、 が指すアドレスにmemcpy
の値を格納する単一の命令に変換します。coliru のライブを参照してください。localval
DataPtr
movl %esi, (%rdi)
命令。