5

同様のトピックとそれに関連するいくつかの資料について、いくつかのクエリを実行しました。しかし、私の質問は主に、以下のコードの警告を理解することです。直したくない!! ユニオンまたはmemcpyの 2 つの方法があることを理解しています。

uint32 localval;
void * DataPtr;
localval = something;
(*(float32*)(DataPtr))= (*(const float32*)((const void*)(&localval)));

以下の重要な点に注意してください
。 1. ここでのキャストに含まれる型は両方とも 32 ビットです。(または私は間違っていますか?)
2. どちらもローカル変数です。

コンパイラ固有のポイント:
1. コードはプラットフォームに依存しないはずです。これは必須です!!
2. GCC でコンパイルしたところ、期待どおりに動作しました。(intをfloatとして再解釈できました)、これが警告を無視した理由です。

私の質問
1. このエイリアシングの場合、コンパイラはどのような最適化を実行できますか?
2. 両方が同じサイズを占有するため (そうでない場合は訂正してください)、そのようなコンパイラの最適化の副作用は何ですか?
3. 警告を安全に無視したり、エイリアシングをオフにしたりできますか?
4. コンパイラが最適化を実行しておらず、最初のコンパイル後にプログラムが壊れていない場合は? コンパイラが同じように動作するたびに(最適化を行わない)、安全に想定できますか?
5. エイリアシングは void * 型キャストにも適用されますか? または、標準の型キャスト (int、float など) にのみ適用されますか?
6. エイリアシング ルールを無効にすると、どのような影響がありますか?

編集済み
1. R および Matt McNabb の修正に基づいて2. 新しい質問を
追加

4

3 に答える 3

3

不完全な例があります(書かれているように、初期化されていないためUBを示しlocalvalます)ので、完成させてください:

uint32 localval;
void * DataPtr;
DataPtr = something;
localval = 42;
(*(float32*)(DataPtr))= (*(const float32*)((const void*)(&localval)));

現在、localvalhas typeuint32*(const float32*)((const void*)(&localval))has typefloat32であるため、エイリアスを作成することはできません。そのため、コンパイラは最後の 2 つのステートメントを相互に自由に並べ替えることができます。これは明らかに、あなたが望むものとは異なる動作になります。

これを書く正しい方法は次のとおりです。

memcpy(DataPtr, &localval, sizeof localval);
于 2014-05-23T05:20:11.910 に答える
3

言語標準は、その言語を使用するプログラマーと、幅広い最適化セットを使用して適度に高速なコードを生成したいコンパイラー作成者との間で、時々競合する利益の間でバランスを取ろうとします。変数をレジスタに保持することは、そのような最適化の 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) このエイリアシングの場合、コンパイラはどのような最適化を実行できますか?

言語標準は、厳密に準拠するプログラムのセマンティクス (動作) について非常に具体的です。それを正しく行うには、コンパイラの作成者または言語の実装者に負担がかかります。プログラマーが一線を越えて未定義の動作を呼び出すと、これが意図したとおりに機能することを証明する責任が、コンパイラーの作成者ではなくプログラマーにあることを標準が明確に示しています。それを行う義務はありませんが、動作が呼び出されました。この時点で「何かが起こる可能性がある」と、通常はジョーク/誇張が続くと、迷惑なことに人々が言うことがあります。あなたのプログラムの場合、コンパイラは「プラットフォームに典型的な」コードを生成する可能性がありますlocalvalsomethingあなたが意図したようにからロードしlocalvalて に保存しDataPtrますが、そうする義務はないことを理解してください。localvalストアを型の何かへのストアとuint32見なし、ロードの逆参照を型(*(const float32*)((const void*)(&localval)))からのロードと見なし、これらが同じ場所にないため、初期化されていないものからロードしている間を含むレジスタにある可能性があるfloat32と結論付けますそのレジスタを予約済みの「自動」ストレージ (スタック) に「スピル」する必要があると判断した場合に予約されるスタック上の場所。ポインターを逆参照してメモリからロードする前に、メモリに格納する場合と格納しない場合があります。コードの次の内容に応じて、それが使用されず、割り当てが決定される場合があります。localvalsomethinglocalvallocalvallocalvalsomething副作用がないため、割り当てが「デッドコード」であると判断され、レジスタへの割り当てさえ行われない場合があります。

2) 両方が同じサイズを占有するため (そうでない場合は修正してください)、そのようなコンパイラの最適化の副作用は何でしょうか?

その結果、 が指すアドレスに未定義の値が格納される可能性がありますDataPtr

3) 警告を安全に無視したり、エイリアシングを無効にしたりできますか?

これは、使用しているコンパイラに固有のものです-コンパイラが厳密なエイリアシング最適化をオフにする方法を文書化している場合は、コンパイラが作成する警告が何であれ、はい。

4) コンパイラが最適化を実行しておらず、最初のコンパイル後にプログラムが壊れていない場合は? コンパイラが同じように動作するたびに(最適化を行わない)、安全に想定できますか?

たぶん、プログラムの別の部分の非常に小さな変更により、コンパイラがこのコードに対して行うことが変わる可能性があります。関数が「インライン化」されている場合、コードの他の部分が混在してスローされる可能性があると考えてください。これを参照してくださいそう質問してください。

5) エイリアシングは void * タイプキャストにも適用されますか? または、標準の型キャスト (int、float など) にのみ適用されますか?

a を逆参照することはできないvoid *ため、コンパイラは最終的なキャストの型を気にするだけです (C++ では、a を a に、またはその逆に変換すると問題が発生constnon-constます)。

6) エイリアシング ルールを無効にすると、どのような影響がありますか?

コンパイラのドキュメントを参照してください。一般に、これを行うと、より遅いコードが得られます (上記の論文の例で Linux カーネルが選択したように)。必要な関数のみを使用して、これを小さなコンパイル単位に制限します。 .

結論

あなたの質問は好奇心のためであり、これがどのように機能するか (または機能しない可能性があるか) をよりよく理解しようとしていることを理解しています。コードが移植可能であることが要件であると述べましたが、暗黙のうちに、プログラムが準拠しており、未定義の動作を呼び出さないことが要件です(そうする場合、負担がかかることを忘れないでください)。この場合、質問で指摘したように、1 つの解決策は を使用することですmemcpy。これは、コードを準拠させて移植可能にするだけでなく、現在の gcc で可能な最も効率的な方法で意図したことを実行することが判明したためです最適化レベル-O3コンパイラはを、 が指すアドレスにmemcpyの値を格納する単一の命令に変換します。coliru のライブを参照してください。localvalDataPtrmovl %esi, (%rdi)命令。

于 2014-05-23T06:08:11.753 に答える
2

違いはconstありません。タイプが同じサイズかどうかを確認するには、 と比較できsizeof (uint32)ますsizeof (float32)。また、2 つのタイプのアライメント要件が異なる可能性もあります。

それらはさておき。フロートが格納されているかのようにメモリを読み取る動作は未定義localvalです。これは、厳密なエイリアシング規則が言うことです。

6.5#6:

格納された値にアクセスするためのオブジェクトの有効な型は、オブジェクトの宣言された型です (存在する場合)。

6.5#7:

オブジェクトは、次の型のいずれかを持つ左辺値式によってのみアクセスされる格納された値を持つものとします

localvalには有効な型uint32があり、「次の型」のリストには含まれていないfloat32ため、これはエイリアシング ルールに違反しています。

動的に割り当てられたメモリでエイリアシングを行っていた場合は、異なります。「宣言された型」がないため、「有効な型」はオブジェクトに最後に格納されたものです。あなたはできますmalloc(sizeof (uint32))し、それにfloat32を格納して読み返すことができます。

要約すると、「これが未定義であることはわかっていますが、コンパイラが正常に動作していることを信頼できますか?」と尋ねているようです。その質問に答えるには、少なくともコンパイラが何であるか、およびそれを呼び出すスイッチを指定する必要があります。

もちろん、厳密なエイリアス規則に違反しないようにコードを調整するオプションもありますが、このトラックを進めるのに十分な背景情報が提供されていません。

于 2014-05-23T05:18:47.030 に答える