想像上の最適化と実際の最適化の10回、SSEの回復、その他すべてについて

昨日 、x = sign(a、b)* min(abs(a)、abs(b))を計算するときの条件付き遷移の最適化に関する1つの投稿に基づいて、おそらく10回。 短い要約:





一般的に、再びベンチマークの古典的な方法論的エラーの束。 誰がそのような間違い、詳細、詳細な報告、最適化を何回もしないこと、そして最も重要なことには、カットされているソースコードをしないことを気にします。



昨日、 元の記事を読みましたが、合成の場合でもトランジションが排除されたため、10倍の加速に非常に驚きました。 これは多すぎます。遷移は高価ですが、それほど多くはありません。 結果を繰り返してみましたが、もっと注意深く、そして当然のことながら、ベンチマーク方法論での幼稚園のエラーです! さて、良い例であるそれらを再び分解する時です。



エラー#1は、 指定されたソースコードが愚かにも正気なものをまったく測定しないことです。 結果の合計llr()がカウントされ、それは良いことです。 しかし、コンパイラは、それがどのような方法でも使用されていないことを認識しているため、最適化するすべての権利を持っています。 最適化したばかりです。 さらに、最初に公開された(現在は整理され修正された)最適化オプションは、間違った結果をまったく考慮せず、見えませんでした。 ああ...



道徳#1:男の子、常に結果を印刷します。 すぐにエラーをキャッチし、コンパイラは「不要な」ループをスローしません。 また、結果をvolatileとして宣言することもできますが、それでも印刷する必要があります。



エラー#2は、作成者がrand()+ llr()ループを測定し、次にrand()+ llr2()ループを測定し、ランタイムを手と目で減算することです。 これは2つの理由で悪い考えです。 randは非常に遅く、ベンチマークは不当に長いことが判明しました。 ;)今回。 この実験では、2つの関数のスタッフィングのパフォーマンスが測定され、このスタッフィングは明らかに、目的の機能としてすぐには動作しません。 これらは2つです。



一般的な場合、「関数A + Bからのスタッフィングを測定しましょう」というアプローチは、コンパイラが計算に干渉する可能性があるため、不適切です。 「測定された」関数Bの一部が関数Aに隠れており、実際にはBから未知の部分を測定していることがわかります。 rand()のようなAテスト構文の代わりに悪いです。



この場合、call_rand()dysasmaでは、インライン化を試みることなく、このような混合は発生しませんが、明らかに他の不幸があります。 どちらかはわかりませんが、CPUについての作業仮説は失速します。 仮説は、esrがrand()から返されたばかりのテストesi、esi命令で始まるllr()計算の開始をわずかに遅らせることにより、元のベンチマークをかなり高速化できるというものです。 同時に、サイクルを2回繰り返すのは簡単です。もちろん、効果はありません。計算を広げる必要があります。



10.7秒、ランド()

13.3秒、rand()+ llr()

12.6秒、rand()+ llr()+ 2xアンロール



// 2x unroll  , 13.3 sec int a = rand() - RAND_MAX / 2; int b = rand() - RAND_MAX / 2; x += LLR(a,b); a = rand() - RAND_MAX / 2; b = rand() - RAND_MAX / 2; x += LLR(a,b); // 2x unroll c , 12.6 sec int a = rand() - RAND_MAX / 2; int b = rand() - RAND_MAX / 2; int a2 = rand() - RAND_MAX / 2; int b2 = rand() - RAND_MAX / 2; x += LLR(a,b); x += LLR(a2,b2);
      
      







ちなみに、実験の1つでは、一般的にx + = LLR(a、b)の前に愚かな挿入asm {nop}によって最大12.8秒の加速が与えられましたが、そのような奇跡を自信を持って繰り返すことはできませんでした。 ある種のランダムな変動。 しかし、一般的に、重いランド()と軽いllr()からの詰め物を測定することは、うーん、取るに足らない仕事であることは明白です。 test / jxx命令のペアのどこかで、式がカウントされたためにストールが発生すると仮定します。 おそらく、VTuneを手にしている人は、より正確に見ることができるでしょう。



道徳#2:男の子は、調査した機能と合成物からひき肉を測定せず、必要な機能のみを測定します。 コンパイラがその詰め物をどのように混合するか、どのような特殊効果がインターフェースに表示されるかは推測ではありません。



ブレーキrand()を取り除き、サイクルごとに十分に大きな乱数ブロックの事前計算を行い、サイクル内では計算と合計llr()のみを残します。 合計で、関数に加えてメモリへのアクセスのオーバーヘッドを測定しますが、線形読み取りでは最小限に抑えられます。 突然、想像上の10倍の加速度ではなく、2.5倍の実際の加速度が観測されます。



わかりました、これはすでに期待に沿ったものですが、現在は十分に速くもよくありません;)



SSEが助けになります。 デスクトップに新しいCore2Duo E8500がありますが、それでもSSE4が可能です。 サイクルを4回拡張し、一度に4ペアをカウントします。 額に直接、符号absの計算に仕様を使用します。



1.073秒、llr()ベースライン

0.438秒、llr2()最適化、2.5倍

0.125秒、llr4()sse + 4xアンロール、8.6x



興味深いことに、コードはかなり読みやすいです。 唯一のことは、_mm_sign_epi32()について慎重にコメントする必要があることです。 最初に、彼は突然2番目の引数も受け取り、1番目の引数の符号で乗算するようにします。 必要なもの。 2番目は、_mm_sign(0)= 0であり、1ではなく0であるため、一部のタスクで必要になります。 しかし、私たちの目的にとっては、おそらく違いはありません。 abs(a)またはabs(b)が0の場合、sign * min(abs)= 0であるため、エラーはありません。



 static inline __m128i LLR4(const int * pa, const int * pb) { __m128i a = *(__m128i*)pa; __m128i b = *(__m128i*)pb; // sign(a,b)*min(abs(a),abs(b)) __m128i absa = _mm_abs_epi32(a); __m128i absb = _mm_abs_epi32(b); __m128i absm = _mm_min_epi32(absa, absb); __m128i ab = _mm_mullo_epi32(a, b); __m128i rr = _mm_sign_epi32(absm, ab); return rr; }
      
      







注意事項:興味深いことに、レジスタをメモリにアンロードしてから4つの通常の整数を追加する代わりに、_mm_hadd_epi32を介してレジスタコンポーネントを合計しても結果は生成されません。 少なくともどこかで、hadの効果を長い間見ていたらよかったのにと思います。



別の注意:興味深いことに、サイクルをさらに発展させて結果は得られません。 どうやら、それはすでにメモリの読み取り速度に依存しており、そこには約6.4 GB /秒が出てきます。



エラー#3は、SSE拡張機能が長い間使用されていないことを意味します。何らかの理由ですべての有効な最適化がプラットフォーム(基本的にi386)に対して行われます。



これは物議を醸す結論である、私は長い間、それを書くべきか、どのように書くべきかを考えた。 それは「間違い」ですか? 個人的にそう思う。 最適化すればとても大きいからです! kamentiの学者は確実に征服しますが、そうではありません。 結局のところ、このバージョンはベクトル化され、一度に4組の数値で動作します。これは、元の関数で明らかに互換性のない変更です。 さらに、i386の最適化も非常に貴重です。突然i7ではなく、IT考古学の記念碑でプログラムが開始されます。 しかし、 いずれにして 、道徳は確かに同じです。



道徳3:男の子、現在2013年、SSE4はほぼどこにでもあり、SSE2はどこにでもあり、それに応じて行動し、1%のフォールバックではなく、99%のユーザーに対して最適化(可能であれば)します。



結果として得られるSSEバージョンは、さまざまな打ち上げの時間が0.125秒から0.145秒までどのように変化するかを見るのに非常に活発です。 差のほぼ20%。 ああ...



エラー#4は最適化のために発生しました;)スレッドとプロセスの優先度を高く設定すると、生産性の約0.5%が得られますが、 ジッターからの測定値は決して保存されません。 まず、アイドル状態のプロセッサは周波数をリセットしますが、すぐには元に戻りません。 彼は0.1秒ではなく10秒以上で成功します。 第二に、 短いテストを100回実行したとしても、個々の反復の時間は依然として踊ります。 これらの100回の実行の開始時と終了時の両方。 99%のアイドル状態にあると想定されるアイドル状態のシステムに、それ自体でまだ機能していること、およびそれがどのように影響するかは、決してわかりません。



プロセッサを「ウォームアップ」する必要があり、その後でも(かなり短い)実行結果をフィルタリングする必要があります。 もう少し繰り返して平均化するだけでは十分ではありませんが、合計時間は依然として著しく震えています。 最初の「コールド」イテレーションを捨てるだけでも十分ではなく、結果は震えています。 平均と分散を考慮し、abs(value-mean)> = k * stddevで外れ値をスローし、平均を再計算できますが、震えています!



そして何よりも、このような単純なメソッドは震えます:数回(10回)実行し、それらから1回の実行の最小時間を選択します。 そのような最小値はスポットに根ざしており、複数の打ち上げの差は最大0.5%であることがわかります。



注意:これらの実行は、3〜10秒間の初期ウォームアップに加えて実行する必要があります。そうしないと、10 * 0.1 = 1秒では列車を設定するのに十分ではなく、最小限のフォーカスにもかかわらず、時間が震えます。 または、10回を超える(強力な)実行が必要ですこの例では、最初のリファレンス実装が同時にこのようなウォームアップとして機能します。 しかし、ベンチ(Test1)の行をコメントアウトすると、聖ヴィートにちなんで名付けられたダンスが再び始まります。 おっと、スロットルスタイル!



道徳#4:男の子、少なくとも3秒間、できれば10秒間パーセントを暖め、さらに少なくともN回を数えます。



ベンチマークでは、まだ多くの間違いを犯す可能性があり、おそらく最終的なコード(ところで、ここではgist.github.com/anonymous/5605201 )に何か他のものが欠けていますが、今日はこれらを整理しました。 完全に役に立たないわけではありません。



警戒し、正しく測定し、容赦なく最適化してください;)



All Articles