C ++エイリアシング、巧妙な最適化、卑劣なバグについて

エイリアシングの現象に関する投稿がここにないことに驚いた。 たぶん、状況を修正する必要があります。 多少複雑なC ++プログラムのエイリアスは、少なくともどこかに必要です。 これは、賢明な最適化の機会を与えてくれる可能性があります。 カッターの下で、両方のケースについて簡単に説明します(もちろん、不変の「コンパイラーが後を打つ」ことはもちろんです;今日、変更のために、それはgccです)。



エイリアシングについて





エイリアシングとは何ですか? とても簡単です。 これは、同じメモリ位置に複数の異なるポインターが表示される場合です。 たとえば。



int A; int * B = &A; int * C = &A;
      
      







この例では、変数Aに突然3つの異なる名前(エイリアス)があります:A、* B、*C。 これは完全に合法なコードです。 コンパイラーは、3つすべての名前を正常に処理します。Aに何かが書き込まれている場合、* Bを介して読み取ることができ、逆も同様です。すべて正常です。



最適化と__restrictについて





1つの小さなことを除いて:可能な最適化。 コンパイラは、そのような視覚的な場合だけでなく、人が暗黙的にエイリアスを想定していない場合にも、エイリアスを理解し、覚えておく必要があります。 たとえば。



 void SumIt ( int * out, const int * in, int count ) { for ( int i=0; i<count; i++ ) (*out) += *in++; }
      
      







outパラメータを使用した通常の機能で、問題を引き起こすものはありません。 しかし、彼女はそうです。 * outアドレスのout-variableが* inアドレスのin-dataと決定的に交差しないことをコンパイラに保証する人はいません。 彼自身にはそのような仮定をする権利がありません。あなたが人がどのようにそしてなぜ書きたいと思うか決してわかりませんか? したがって、最適化の最大レベルであっても、内部ループの各反復で* outがメモリに書き戻されます。 disasm(gcc -O3 -S、4.4.3、ubuntu x64)は次のようになります。



 .L7: addq $4, %rax addl (%rsi), %ecx cmpq %rdx, %rax movq %rax, %rsi movl %ecx, (%rdi) ; <--  ! jne .L7
      
      







ただし、outがinと交差しないことをコンパイラーに伝えることができます。 このため、人類は__restrict修飾子を思いつきました



 void SumIt ( int * __restrict out, const int * __restrict in, int count ) { for ( int i=0; i<count; i++ ) (*out) += *in++; }
      
      







 .L14: addq $4, %rax addl (%rsi), %ecx cmpq %rdx, %rax movq %rax, %rsi jne .L14 movl %ecx, (%rdi)
      
      







さて、1つの命令ですか? プロセッサは、シックキャッシュと多数のパイプラインを備え、スマートになりました。 このミニ例では、もちろん、レコードは即座にキャッシュされ、追加の命令は何かと並列化されることになっており、その差は測定することすらできません。



 1783293664 in 103818 usec 1783293664 in 69818 usec
      
      







そうでもない。 成功しました。 おっと、加速度は約1.5倍です。 これは、1つの命令(および2つの修飾子)の価格である場合があります。 通常はすべて同じですが、負荷の高い内部ループには便利です。



厳密なエイリアスとバグについて





ご覧のとおり、エイリアシングを除去すると、速度が大幅に向上する可能性があります。 どうやら、これらの考慮事項から、C99標準、およびこのC ++を通じて、 厳密なエイリアスルールが考案され、導入されました。 規格を読んで理解するスキルを持つ人々のためのリファレンス:N1124、6.5(7)。 通常の人は実際にそこを見たくありません。たとえば、この段落には厳密な単語やエイリアスはありません。 ;)(脚注番号74にエイリアスされた単語が含まれているため、それを何とかすばやく見つけることができました。)しかし、特に重要な「指で」という意味は非常に簡単に説明できます。



厳密なエイリアスモードでは、コンパイラは、「実質的に異なる」型のポインタが指すオブジェクトを同じメモリ位置に格納できないと考えており、これを最適化に使用できます。



ポインターが実際に異なる場所に表示されている場合、または互いに十分に離れて使用されている場合、これは絶対に重要ではありません。 しかし、致命的なのは、ポインターが同じメモリを指す場合、それらは並行して使用され、コンパイラーはgccであるということです。



 #include <stdio.h> typedef unsigned int DWORD; typedef unsigned short WORD; inline DWORD SwapWords ( DWORD val ) { WORD * p = (WORD*) &val; WORD t; t = p[0]; p[0] = p[1]; p[1] = t; return val; } int main() { printf ( "%d\n", SwapWords(1) ); return 0; }
      
      







この単純なプログラムは、g ++ test.cppのビルド時に65536を出力するか、g ++ -O3 test.cppのビルド時に1を出力します。 なんてこった!



実際のところ、-O2で始まる-fstrict-aliasingは自動的にオンになります。 コンパイラは、* pは基本的にvalの格納場所を示すことができないと考えています。 そして成功して、このビジネスは死を最適化します。それができない場合、引数の値が返されます。 したがって、SwapWords(1)は定数1で簡単に置き換えることができます。



さらに、この例では、問題は実際にはそれほどではありません。 -Wall(または少なくとも-Wstrict-aliasing)を有効にすると、コンパイラーは理解できないものについて正直に文句を言います。



 test.cpp:8: warning: dereferencing pointer 'p.14' does break strict-aliasing rules
      
      







修正するのは難しくありません。 幼稚園の方法では、-fno-strict-aliasingキーを使用して、完全なエイリアスを完全に無効にします。 事実上すべての場所で機能する法的な改善方法は、組合を通じて価値を拡大することです。 コンパイラーは、共用体フィールドが相互にエイリアスすることを容赦なく許可しているようです。 ポインターを使用したトリックは、理論的には横向き(未定義の動作)になる可能性があります。gccの場合、理論を実践することは難しくなく、逆も同様です(-fstrict-aliasing)。



 inline DWORD SwapWords ( DWORD val ) { union { DWORD d; WORD v[2]; } u; ud = val; WORD t = uv[0]; uv[0] = uv[1]; uv[1] = t; return ud; }
      
      







やった! しかし、悲しいかな、1つだけありますが、 -Wstrict-aliasingは何も保証しません 。 現在のコンパイルモードと互換性のないエイリアスのすべてのケースをキャッチするだけでは不十分です。 私は十分に短いので説明的な例がないので(長すぎます)、言葉を使わなければなりません:かなりの詰め物の肉、ファンクター、または2つ、そして厳密なエイリアシングは巧妙に偽装されており、ヴォーニングを与えません。 STLやBoostを積極的に使用するプログラムでは、コードの荒野のどこかにある厳密なエイリアスに慎重に違反するのはかなり簡単だと思います。 また、サードパーティは、無効なコード*の生成を残しながら、少なくともgcc 4.1.xでvoid *へのキャストのトリックが正常に警告を抑制したことを証言しています。



未定義の動作にもかかわらず、ネジはもちろんフォーマットしません。 (まあ、すぐではありません。)ただし、コンパイラは最適化のために、メモリへの読み取りと書き込みを簡単に交換できます。 こんな感じです。



 inline uint64_t GetIt ( const DWORD * p ) { return *(uint64_t*)p; } int main() { DWORD buf[10]; uint64_t t; buf[0] = 1; buf[1] = 2; t = GetIt(buf); buf[2] = 3; buf[3] = 4; printf ( "%d, %d, %d, %d\n", buf[0], buf[1], int(t>>32), int(t) ); return 0; }
      
      







1、2、32573、-648193368という少し予想外の数値が再び出力されます。 コンパイラーがSwapWords()関数を関数の完全な不在に単純化した前の例とは異なり、これはまさに読み書きが再配置される場所です。 コンパイラは、GetIt(buf)はbufの内容に依存しないと結論付け、したがって、適切と思われる場所にGetIt()に「呼び出し」をポップします。 バッファを充填する前に必要であることが判明しました。



 mov (%rsp),%r8 ; t = GetIt(buf) ... movl $0x1,(%rsp) ; buf[0] = 1 movl $0x2,0x4(%rsp) ; buf[1] = 2 movl $0x3,0x8(%rsp) ; buf[2] = 3 movl $0x4,0xc(%rsp) ; buf[3] = 4 mov %r8d,%r9d ; r9 = int(t) shr $0x20,%r8 ; r8 = int(t>>32) callq 0x400510 <__printf_chk@plt>
      
      







その結果、一部の変数に無効な値(古すぎるか、または新しすぎる)が表示され、その後に続くすべての変数が表示されます。 このような下劣なバグを長い間、成功せずにキャッチすることができます。明示的に成功するには、オプティマイザーはこの「ブラインド」な場所でw1xの最適化を実行する必要があります。最適化は結果がw2xでキャッチされるように表示されます。



自信を持って戦う方法は? 適切な自動方法とツールがわかりません。 コンパイラは多かれ少なかれキャッチするものだと思っていました。 しかし、今では、テンプレートでわずかにラップされている場合(または関数でさえも)、完全に些細な変換をスキップすることもできます。 祈りと断食は厳しい規律なので戦ってください。 ポインターのタイプを変更し、副作用について考えます。 コンパイラーのように感じてください。キツネが走っているのか、ワシが飛んでいるのか、エイリアシングが壊れているのか、ちょっとした男を考えてください。



サイドノート:厳密なエイリアシングによるその他のあらゆる微妙さとトリックについては、同志に関する古典的な詳細な投稿を読むことができます。 マイク・アクトン、 cellperformance.beyond3d.com / articles / 2006/06 / understanding-strict-aliasing.html



MSVCはどうですか?





厳密なエイリアシングについては問題ありません。 また、オンにすることもできません。 どうやら、MSは、世界のエイリアシングについての希薄性のない世界のC99準拠のコードは、通常よりもはるかに少ないと判断したため、複雑さを作成する必要はありません。 教育的な任務はgccによって実行され、時には静かに行われますが、2番目に、それがないわけではありません。



これは自動的に最適化とトリックの__restrictについてのトリックがやや重要であることを意味します。 void SumIt(int64_t * out、const int * in、int count)の場合、厳密な規則に従って、gccにはoutが中にある可能性が低いと「推測」する権利があります。 MSVCはこれについて推測することを保証されません。 手動で制限するか、エントリを手動で最小化する必要があります。 彼は、ローカル変数をレジスタに入れることができます。



クラスのメンバーはthisポインターにあるデータでもあることを理解することが重要です。 したがって、ループ内のクラスメンバーへの定数参照は、メモリの一定の苦痛にコンパイルできます。



合計





1.内部ループを記述し、エイリアスと__restrictについて覚えています。

2.ポインターを変換し、厳密なエイリアシングと副作用を再配置する核の可能性について覚えておいてください。

3. gccを使用します。デフォルトの-fstrict-aliasingを忘れないでください。-Wallは無視しないでください。

4. MSVCを使用し、厳密なエイリアススタイルの最適化が強制的に存在しないことを覚えて、手作業で最適化します。

5.厳密なエイリアシングに関する警告が表示されたら、それを整理します。おそらくUBスレッドです。

6.厳密なエイリアシングに関する警告は表示されませんか? しかし、彼はそうです。 ホリネズミのように。

6.1。 コンパイラーは嘘をつき、Voringsは信頼性が低く、-Wallは時々動作します(オンにする必要があります!)、しかし保証はしません。

6.2。 あなたはまったく自分自身を信頼することはできません。ベンチマーク、不満、祈り、断食だけです。



All Articles