4

コードは次のようになり、内側のループには膨大な時間がかかります。

#define _table_derive                  ((double*)(Buffer_temp + offset))
#define Table_derive(m,nbol,pos)        _table_derive[(m) + 5*((pos) + _interval_derive_dIdQ * (nbol))]
char *Buffer_temp=malloc(...);

for (n_bol=0; n_bol<1400; n_bol++)  // long loop here
    [lots of code here, hundreds of lines with computations on doubles, other loops, etc]

    double ddI=0, ddQ=0;

    // This is the original code
    for(k=0; k< 100; k++ ) {
            ddI += Table_derive(2,n_bol,k);
            ddQ += Table_derive(3,n_bol,k);
    }
    ddI /= _interval_derive_dIdQ;
    ddQ /= _interval_derive_dIdQ;
    [more code here]
}

oprofile は、実行時間のほとんどがここで費やされていることを示しています (2 番目の列は時間の % です)。

129304  7.6913 :for(k=0; k< 100; k++) {
275831 16.4070 :ddI += Table_derive(2,n_bol,k);
764965 45.5018 :ddQ += Table_derive(3,n_bol,k);

私の最初の質問は、コードが遅い適切な場所を示すために oprofile に頼ることができますか (-Og と -Ofast で試しましたが、基本的に同じです)。

2 番目の質問は、この非常に単純なループが sqrt、atan2、およびその前にある数百行の計算よりも遅いのはなぜですか? すべてのコードを表示しているわけではないことはわかっていますが、コードがたくさんあり、意味がわかりません。

ベクトル化(機能しない)またはアンロール(機能する)するためにさまざまなオプティマイザーのトリックを試しましたが、ほとんど得られませんでした。たとえば、次のようになります。

    typedef double aligned_double __attribute__((aligned(8)));
    typedef const aligned_double* SSE_PTR;
    SSE_PTR TD=(SSE_PTR)&Table_derive(2,n_bol,0);   // We KNOW the alignement is correct because offset is multiple of 8

    for(k=0; k< 100; k++, TD+=5) {
        #pragma Loop_Optimize Unroll No_Vector
        ddI += TD[0];
        ddQ += TD[1];
    }

オプティマイザの出力を確認しました: "-Ofast -g -march=native -fopt-info-all=missed.info -funroll-loops" この場合、"loop unrolled 9 times" が表示されますが、ベクトル化しようとすると、(要するに)次のようになります。 ) _1328 + (long unsigned int) (n_bol_1173 * 500) * 2) * 4)"

これをスピードアップする方法はありますか?

補遺:コメントをありがとう、私はここで答えようとします:

  • はい、私はコードが醜いことを知っています (それは私のものではありません)。
  • Cコードがライブラリにあり、Cによって処理および変更されると大きな配列が呼び出し元(IDL、Python、またはC)に渡されるため、私はこの配列にこだわっています。
  • char* を複雑な多次元の double* にキャストする代わりに、いくつかの構造体を使用する方がよいことはわかっていますが、上記を参照してください。このプログラムが最初に作成されたとき、構造体は C 仕様の一部ではなかった可能性があります (冗談です... 多分)
  • ベクトライザーの場合、構造体の配列よりも配列の構造体の方が良いことはわかっていますが、ため息...上記を参照してください。
  • (呼び出しプログラム内に) 実際の外部ループがあるため、このモノリシック配列の合計サイズは約 2Gb です。
  • 現状では、最適化なしで実行するのに約 15 分かかり、いくつかのコードを書き直してから 1 分後 (atan2 の高速化、配列内の手動整列など)、-Ofast と -march=native を使用しました。
  • ハードウェアの制約が変更されたため、データフローに追いつくために高速化を試みています。
  • Clang を試してみたところ、ゲインはわずか (数秒) でしたが、-fopt-info などの最適化レポートを取得するオプションが表示されません。何が起こっているのかを知る唯一のオプションとしてアセンブリを見る必要がありますか?
  • システムは 500Gb の RAM を備えた猛烈な 64 コアですが、上記のコードを並列化するための OpenMP プラグマを挿入することはできませんでした (私は試しました): ファイルを読み取り、メモリ内で完全に解凍します (2Gb) 、それを順番に分析し (「+=」など)、呼び出し元の IDL/Python にいくつかの結果を吐き出します。すべてが単一のコア上にあります (ただし、他のコアは実際の取得と後処理でかなりビジーです)。:(
  • 役に立たない、素晴らしい提案をありがとう: ddQ += ... を削除すると、時間の割合が前の行に転送されるようです: 376280 39.4835:ddI+=...
  • これにより、さらに良い結果が得られます。両方を削除すると(したがってループ全体が)保存されます...何もありません!!! ピーターが言ったように、私はプロファイラーを信用できないと思います。ループレス プログラムをプロファイリングすると、タイミングがより均等に分散されます (以前は 1 秒を超える 3 行だけでしたが、現在は約 10 行で、単純な変数の割り当てのようにすべて無意味です)。

内側のループは最初からニシンだったと思います。手動タイミングを使用して最適化を再開します。ありがとう。

4

1 に答える 1

3

私の最初の質問は、コードが遅い適切な場所を示すために oprofile に頼ることができますか?

正確ではありません。私が理解しているように、サイクルは、入力の生成や他の実行リソースの解放が遅い命令ではなく、入力を待機している命令 (または他の実行リソース) に課金されることがよくあります。

ただし、oprofile の出力では、実際には最後のループである可能性があります。この外側のループ内に他の内側のループはありますか?

キャッシュミスをプロファイリングしましたか? サイクル以外にも多くの興味深いカウンターがあります。

また、パフォーマンスを本当に理解するには、C ではなく asm のプロファイル アノテーションを確認する必要があることに注意してください。たとえば、一方が他方より多くの時間を追加するのは奇妙ですが、それはおそらく insns をマッピングする問題にすぎません。ソース行。


re: perf はループをコメントアウトした結果です:

では、その内部ループがなければ、プログラムはまったく高速に実行されないのでしょうか? 外側のループが既にそのメモリにアクセスしている場合は、キャッシュ ミスがボトルネックになっていて、内側のループが再びそのメモリにアクセスしていたのではないでしょうか? 試してみてperf record -e L1-dcache-load-misses ./a.outくださいperf report。またはoprofile同等のもの。

おそらく、内側のループの uops は、外側のループの低速な処理がリタイアするまで発行を待機してスタックしていたのでしょう。最新の Intel CPU の ReOrder Buffer (ROB) サイズは約 200 uop であり、ほとんどの insn は単一の uop にデコードされるため、アウトオブオーダー ウィンドウは約 200 命令です。

内側のループをコメント アウトすることは、内側のループの実行中に、外側のループのループに含まれる依存関係チェーンを完了する時間がないことも意味します。その内側のループを取り除くと、スループットからレイテンシまで、外側のループのボトルネックに質的な変化が生じる可能性があります。


re: で 15 倍速くなり-Ofast -march=nativeます。OK]それは良いことだ。最適化されていないコードは恐ろしいものであり、いかなる種類の「ベースライン」またはパフォーマンスのためのものとも見なされるべきではありません。何かと比較したい場合は、 (自動ベクトル化、 、または-O2を含まない) と比較してください。-ffast-math-march=native

-fprofile-generate/を使ってみてください-fprofile-use。profile-use には が含まれ-funroll-loopsているため、プロファイリング データが利用可能な場合に最適に機能すると思います。

re: 自動並列化:

OpenMP プラグマまたはのようなgcc オプション-floop-parallelize-all -ftree-parallelize-loops=4を使用して、具体的に有効にする必要があります。自明でないループ運搬依存関係がある場合、自動並列化は不可能な場合があります。その wiki ページも古く、自動並列化の最先端を反映していない可能性があります。どのループを並列化するかについての OpenMP のヒントは、特にコンパイラーに推測させるよりも賢明な方法だと思います。なし-fprofile-use


Clang を試してみたところ、ゲインはわずか (数秒) でしたが、-fopt-info などの最適化レポートを取得するオプションが表示されません。何が起こっているのかを知る唯一のオプションとしてアセンブリを見る必要がありますか?

clang マニュアルにはclang -Rpass=inlineインライン化に関するレポートに 使用できると書かれています。llvm のドキュメントでは、ベクトル化パスの名前は であると記載されているため、 , または をloop-vectorize使用して、どのステートメントがベクトル化に失敗した-Rpass-missed=loop-vectorizeかを知ることができます。-Rpass-analysis=loop-vectorize

asmを見ることは、自動ベクトル化がうまくいかなかったかどうかを知る唯一の方法ですが、コンパイラーの作業を実際に判断するには、効率的な asm を自分で記述する方法を知る必要があります (したがって、それが何を行うことができたかをおおよそ知ることができます) 。httpを参照してください。 ://agner.org/optimize/、およびタグ wiki の他のリンク。

コードをhttp://gcc.godbolt.org/に置いて別のコンパイラで試してみることはしませんでしたが、例で asm が完全なソースから見たものを代表するものである場合は、リンクを投稿できます。


自動ベクトル化

for(k=0; k< 100; k++ ) {
        ddI += Table_derive(2,n_bol,k);
        ddQ += Table_derive(3,n_bol,k);
}

2 と 3 は連続する要素であるため、これは自動ベクトル化する必要があります。テーブルを複数のテーブルに分割すると、(この部分の) キャッシュの局所性が向上します。たとえば、5 つの各グループの要素 2 と 3 を 1 つの配列に保持します。一緒に使用される他の要素をテーブルにグループ化します。(オーバーラップがある場合、たとえば、別のループで要素 1 と 3 が必要な場合、自動ベクトル化できないループを分割することはできますか?)

re: 質問の更新: SSE で自動ベクトル化するために、配列の構造体は必要ありません。16B ベクトルは正確に 2 つdoubleの を保持するため、コンパイラは[ ddI ddQ ]withのベクトルを累積できaddsdます。AVX 256b ベクトルでは、単一の 256b ロードの代わりに、vmovupd/vinsertf128を実行して隣接する構造体から s のペアを取得する必要doubleがありますが、大したことではありません。ただし、メモリの局所性は問題です。doubleタッチするキャッシュラインで5 秒ごとに 2 秒しか使用していません。


-ffast-math倍精度ベクトルを持つ CPU をターゲットにしている限り、 がなくても自動ベクトル化する必要があります。(例: x86-64、または 32 ビット-msse2)。

gcc は、アライメントされていない可能性のあるデータに対して大きなプロローグを作成することを好み、アライメントされたアドレスに到達するまでスカラーを使用します。これは肥大化したコードにつながります。256b ベクトルと小さな要素を使用します。ただし、それほど悪くはないはずdoubleです。それでも、clang 3.7 または clang 3.8 を試してみてください。clang は、アラインされていない可能性のあるアクセスをアラインされていないロードで自動ベクトル化します。データが実行時にアラインされる場合、追加のコストはかかりません。(gcc は、データが整列されていないというまれなケースを最適化します。これは、整列されていないロード/ストア命令が、整列されたデータに対して使用された場合でも、古い CPU (たとえば、Intel Nehalem より前) では遅くなったためです。)


それぞれが 8B アラインされていることを証明できない場合、char配列が自動ベクトライザーを無効にしている可能性があります。double@JohnBollingerがコメントしたように、それは本当に醜いです。5 つの double の構造体の配列がある場合は、そのように宣言してください!

構造体の配列として記述する方法:

「手動」の多次元インデックス付けを維持しますが、基本 1D 配列を の配列double、またはそれ以上のstruct型にすることで、コンパイラはすべてdoubleの配列が 8B 境界であると想定します。

Buffer_temp元のバージョンは、配列へのアクセスごとにグローバルも参照していました。(それともローカルでしたか?) 別名を持つ可能性のあるすべてのストアでは、ベース ポインターを再ロードする必要があります。(C のエイリアシング ルールではchar*、何でもエイリアスできますが、逆参照する前に a にキャストすると、それを回避できると思いますdouble*。いずれにせよ、内側のループ内の配列に格納することはありませんが、外側の配列にいると仮定します。)

typedef struct table_derive_entry {
    double a,b,c,d,e;
} derive_t;

void foo(void)
{
    // I wasn't clear on whether table is static/global, or per-call scratch space.
    derive_t *table = aligned_alloc(foo*bar*sizeof(derive_t), 64);            // or just malloc, or C99 variable size array.

    // table += offset/sizeof(table[0]);   // if table is global and offset is fixed within one call...

// maybe make offset a macro arg, too?
#define Table_derive(nbol, pos)     table[offset/sizeof(derive_t) + (pos) + _interval_derive_dIdQ / sizeof(derive_t) * (nbol))]


    // ...        
    for(k=0; k< 100; k++ ) {
         ddI += Table_derive(n_bol, k).b;
         ddQ += Table_derive(n_bol, k).c;
    }
    // ...
}
#undef Table_derive

_interval_derive_dIdQoffsetが常に 5 * 8B の倍数ではない場合double *table = ...;、Table_derive を次のように宣言して変更する必要がある場合があります。

#define Table_derive(nbol, pos)   ( ((derive_t *)(double_table + offset/sizeof(double) + _interval_derive_dIdQ / sizeof(double) * (nbol)))[pos] )

FP部門:

ddI /= _interval_derive_dIdQ;
ddQ /= _interval_derive_dIdQ;

double inv_interval_derive_dIdQ = 1.0 / _interval_derive_dIdQ;ループから抜け出すことができますか? 乗算は、特に除算よりも大幅に安価です。レイテンシが重要な場合、または div ユニットも sqrt に必要な場合。

于 2016-04-28T01:10:48.547 に答える