2012-06-07 16 views
18

まず、このタイプの質問がよく聞かれるので、私ができる限り多くのことを読んだと言ってこれを序文にしておきましょう。OpenMPの性能

私は大量ののforループを並列化しました。ループの反復回数は20から150までの間で変化しますが、ループ本体は膨大な量の作業を行いますが、多くの局所的な線形代数ルーチンを呼び出します(コードはソースの一部であり外部依存関係ではありません) 。ループ本体には、これらのルーチンへの1000回以上の呼び出しがありますが、それらはすべて互いに完全に独立しているため、並列処理の主な候補になると考えました。ループコードはC++ですが、Cで書かれたサブルーチンがたくさん呼び出されます。

コードは次のようになります。

<declare and initialize shared variables here> 
#ifdef _OPENMP 
#pragma omp parallel for       \ 
    private(....)\ 
    shared(....)    \ 
    firstprivate(....) schedule(runtime) 
#endif 
    for(tst = 0; tst < ntest; tst++) { 

    // Lots of functionality (science!) 
    // Calls to other deep functions which manipulate private variables only 
    // Call to function which has 1000 loop iterations doing matrix manipulation 
    // With no exaggeration, there are probably millions 
    // of for-loop iterations in this body, in the various functions called. 
    // They also do lots of mallocing and freeing 
    // Finally generated some calculated_values 

    shared_array1[tst] = calculated_value1; 
    shared_array2[tst] = calculated_value2; 
    shared_array3[tst] = calculated_value3; 

} // end of parallel and for 

// final tidy up 

、私は信じて、すべての任意の同期があってはならない - スレッドが共有変数にアクセスする唯一の時間shared_arraysであり、それらは、それらの配列にユニークなポイントにアクセスし、tstでインデックス化。

これは、マルチコアクラスタ上のスレッド数を上げたときのことです(私たちがこのループを5回呼び出す場所)の速度は次のとおりです。

   Elapsed time System time 
Serial:  188.149   1.031 
2 thrds:  148.542   6.788 
4 thrds:  309.586  424.037  # SAY WHAT? 
8 thrds:  230.290  568.166 
16 thrds:  219.133  799.780 

目立つかもしれ物事は2と4スレッド間のシステム時間の大幅なジャンプ、実際の経過時間は、私たちが2から4に移動兼ね、その後、徐々に減少しています。

OMP_SCHEDULEという膨大な範囲のパラメータを試しましたが、運はありませんでした。これは、各スレッドがmalloc/newとfree/deleteをたくさん使用しているという事実に関連していますか?これは8GBのメモリで一貫して動作していますが、それは問題ではないと思います。率直に言って、システム時間の巨大な増加は、スレッドがブロックしているように見えますが、なぜそれが起こるのかわかりません。ループはスレッドローカル配列での計算された値を格納し、その後に渡ってこれらの配列をコピーするように、私は本当に偽共有が問題になるだろうと思ったので、再度書いたコード

UPDATE 1 最後に共有配列。残念ながら、私はほとんどそれを信じていませんが、これは影響はありませんでした。

はcmeerwのアドバイス、@の後、私はstraceの-fを実行し、すべての初期化後に

[pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58066] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 58065] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 57684] <... futex resumed>)  = 0 
[pid 58067] <... futex resumed>)  = 0 
[pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58066] <... futex resumed>)  = 0 
[pid 57684] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 58065] <... futex resumed>)  = 0 
[pid 58067] <... futex resumed>)  = 0 
[pid 57684] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58066] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58065] <... futex resumed>)  = 0 
[pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 57684] <... futex resumed>)  = 0 
[pid 58067] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 58066] <... futex resumed>)  = 0 
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58067] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58065] <... futex resumed>)  = 0 
[pid 58067] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58066] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 57684] <... futex resumed>)  = 0 
[pid 58067] <... futex resumed>)  = 0 
[pid 58066] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58065] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 58066] <... futex resumed>)  = 0 
[pid 58065] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 58066] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 57684] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 58067] futex(0x35ca58bb40, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...> 
[pid 58066] <... futex resumed>)  = -1 EAGAIN (Resource temporarily unavailable) 
[pid 58065] futex(0x35ca58bb40, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> 
[pid 57684] <... futex resumed>)  = 0 

のラインのちょうど百万人がある誰もが何を意味するのか任意のアイデアがありますか?スレッドがコンテキスト切り替えの頻度が高すぎるように見えますか、またはブロックとブロック解除だけですか? straceOMP_NUM_THREADSが0に設定された同じ実装では、これはまったくありません。比較のために、1つのスレッドが使用されたときに生成されるログファイルは486 KBであり、4つのスレッドが使用されたときに生成されるログファイルは266 MBです。言い換えれば

、並列バージョンは、ログファイルの余分4170104行を呼び出す...

UPDATE 2

トムによって示唆されるように、私は無駄に特定のプロセッサに結合スレッドを試してみました。私たちはOpenMP 3.1を使用していますので、export OMP_PROC_BIND=trueを使って環境変数を設定しました。同じサイズのログファイルと同じ時間枠。

UPDATE 3

プロットが厚くなります。これまでのところ、クラスタでしかプロファイルできなかったので、私はMacports経由でGNU GCC 4.7をインストールし、初めてMacbookにコンパイルしました(AppleのGCC-4.2.1はOpenMPが有効なときにコンパイラのバグを投げます。これまでコンパイルして並列実行していなかった)。 MacBookの上では、基本的に、あなたが、これは我々がデータを持っている。このテストで反復処理しているデータ・セットのカップルとしてほとんど驚くべきことであるものの

   C-code time 
Serial:   ~34 seconds 
2 thrds:  ~21 seconds 
4 thrds:  ~14 seconds 
8 thrds:  ~12 seconds 
16 thrds:   ~9 seconds 

我々は、両端に向けてdimishingリターンを参照してください期待トレンドを見ます< 16人のメンバーです(例えば、for-loopで7回の繰り返しで16個のスレッドを生成しています)。

これで問題は残ります。なぜクラスタのパフォーマンスが低下するのですか?私は今夜​​別のquadcore linuxboxを試してみるつもりです。クラスターはGNU-GCC 4.6.3でコンパイルされますが、それ自体がそのような違いを生み出すとは思いませんか?

ltraceももクラスタにインストールされていません(さまざまな理由でそれらを取得できません)。私のlinuxboxがクラスタのようなパフォーマンスを与えるなら、対応するltrace分析を実行します。

UPDATE 4

私のああ。私はMacBook ProをUbuntu(12.04)にブートしてコードを再実行しました。それはすべて実行されています(これはいくぶん安心しています)が、私はクラスタで見たものと同じ、奇妙でない悪いパフォーマンスの振る舞い、そして何百万もの同じ実行がfutexの呼び出しを参照しています。 Ubuntuの私のローカルマシンとOSXの唯一の違いはソフトウェアだ(と私は同じコンパイラとライブラリを使用しています - おそらく、OSXとUbuntuの異なるglibcの実装はありません!)私は今これが何かLinuxがスレッドをどのようにスケジューリング/配布するかに関係します。どんな場合でも、私のローカルマシン上にあることは何百万回も簡単になるので、私は先に進み、ltrace -fとそれを見て私が見つけることができるものを見ます。私は別のプロセスからforks()のクラスタを回避する方法を書いており、ランタイムでは完全な1/2を与えているので、並列性を得ることは間違いありません。

+2

これらの関数で乱数生成を行っていますか?同様の問題がありました:glibcはrandへの呼び出しごとに同期を行います。 OpenMPのためのソリューション:http://stackoverflow.com/questions/8980056/splitting-up-a-program-into-4-threads-is-slower-than-a-single-thread –

+0

@AlexanderKondratskiyいいえ、しかし、ありがとうございました簡単にされている! – Alex

+2

"strace -f"の下で実行しようとしましたか?少なくとも、どのシステムコールが関係しているかを示す必要があります。 – cmeerw

答えて

8

プロファイリング用のプロダクションレベルのコードを生成するための大きなラッパー関数を作成することに関連してかなり広範なプロファイリング(gprofとgdbでの時間情報のサンプリングのためthis great postのおかげで)の大部分は、私がgdbで実行中のコードを中止し、backtraceを実行したとき、スタックは何らかの方法でベクトルを操作してSTL <vector>コールにあった。

コードは、プライベート変数としてparallelセクションにいくつかのベクトルを渡します。これはうまくいくようです。しかし、すべてのベクトルを取り出して配列(それらを動作させるためにいくつかの他の偽造ポケリー)で置き換えた後、私はかなりのスピードアップを見た。小さい、人工的なデータセットでは、実際のデータセットではスピードアップはあまり良くありませんが、スピードアップはほぼ完璧です(すなわち、スレッドの倍数を半分にすると)どのようにコードが動作するのか。

数十万回の繰り返しでループが動いているときに、何らかの理由で静的な変数やグローバル変数があると思われますが、Linuxでは深刻なレベルのロックがあります(Ubuntu 12.01およびCentOS 6.2)、OSXでは認識されません。

私はなぜこの違いがあるのか​​興味があります。 STLがどのように実装されているか(OSXのバージョンはGNU GCC 4でコンパイルされていました)の違いかもしれません。Linuxのように7)、コンテキスト切り替え(Arne Babenhauserheideが提案したように)との関係です。

  • 初期プロファイリング

  • が識別としてロックで本当に便利でした

  • strace -fltrace -fでプロファイル共有変数として機能して何static変数が存在しない保証問題を識別するためR内から犯人

  • valgrindエラー

  • スケジュールタイプ(自動、ガイド付き、静的、動的)とチャンクサイズのさまざまな組み合わせを試しました。次いで、特定の値のためのスレッドローカルバッファを作成することにより、偽シェアリングを避けたプロセッサ

  • 、及びへ

  • 試み結合スレッドが削除 for-loop

  • の端部に単一の同期イベントを実装するすべてのmallocing並列領域内のfreeing - 問題の解決には役立たなかったが、小さな一般的な高速化を提供した。

  • さまざまなアーキテクチャとOSsesで試行された - rしかし、これはLinux対OSXの問題であり、スーパーコンピュータ対デスクトップの問題ではないことを示した。

  • fork()呼び出しを使用して並行性を実装するバージョンを構築する。これは

  • gdbの時間プロファイリングをサンプリング(中止とバックトレース)

  • プロファイリングgprofの生産データのロード

  • を複製するデータシミュレータを内蔵

  • 良いだった、OSXとLinuxの両方で時間を半減しました

  • ベクトル演算

  • この働いなかったならばコメントアウトし、それがうまくので、持っていることのように見えますがArne Babenhauserheide's link私はOpenMPのメモリ断片化の問題に重大な問題を抱えています

+0

これらのベクトル演算が何であるか知ることは非常に面白いでしょう。繰り返しベクトルのサイズを変更しようとしましたか(もちろん、メモリ割り当てによる同期が必要です)。 – cmeerw

+0

@cmeerw^yup。私は、メモリ割り振りが常に同期なしで別々のスレッドで起こっていることを考えれば、それらがすべて別々のスレッドにあると仮定していました。 – Alex

+0

@Alex:良い答えをありがとう。さて、「Rの中からの初期プロファイリング」[...]「...あなたは統計プログラムを意味しますか?どうやって?!どのような魔術はこれです! :) –

4

確かに分かりません重要なプロファイリングなしで起こったが、性能曲線がFalse Sharingを示すようだ...

スレッドが異なるオブジェクトを使用しますが、これらのオブジェクトは、彼らが同じキャッシュラインに入ることメモリに 十分に近いことが起こると、キャッシュ システムはそれらを1つのコアだけが保持できる ハードウェア書き込みロックによって効果的に保護される単一の塊として扱います特に博士ドブス

http://www.drdobbs.com/go-parallel/article/217500206?pgno=1

ルーチンは/無料malloc関数の多くを行っているという事実で話題の時

素晴らしい記事は、これにつながる可能性があります。

解決策の1つは、デフォルトのアロケータではなくプールベースのメモリアロケータを使用して、各スレッドが異なる物理アドレス範囲からメモリを割り当てる傾向があることです。

+8

しかし、システム時間の増加は、誤った共有よりも競合のロックを指し示すことになります(誤った共有は、システム時間ではなくユーザ時間の増加に現れます)。 – cmeerw

+0

@ cmeerw:理由を詳しく説明できますか? –

+1

Linuxでは、未使用のロックは完全にユーザー空間で行われます(これらはシステム時間に表示されません)。一方、競合したロックはスレッドを中断/再開するためにカーネルを呼び出す必要がありますfutexシステムコール)。これは、straceの出力がはっきりと示されているものです(実際、フューテックスのシステムコールの最初の引数は常に同じですが、それは単なるロックです)。一方、誤った共有はコードの速度を低下させるだけですが、追加のシステムコールを導入しないため、主にユーザー時間の減速が見込まれます。 – cmeerw

1

スレッドは実際には相互作用しないので、コードをマルチプロセッシングに変更するだけで済みます。最後にメッセージを渡すだけで、スレッドが何かを同期させる必要がないことが保証されます。

これは基本的にこれを実行するpython3.2コードです(パフォーマンス上の理由からpythonでやってほしくないかもしれませんし、forループをC関数に入れてcython経由でバインドすることもできます)。なぜコードをPythonで表示するのかを参照してください):

from concurrent import futures 
from my_cython_module import huge_function 
parameters = range(ntest) 
with futures.ProcessPoolExecutor(4) as e: 
    results = e.map(huge_function, parameters) 
    shared_array = list(results) 

これはそれです。プロセス数をクラスタに入れることができるジョブ数に増やし、各プロセスがジョブをサブミットして監視して、任意の数のコールにスケーリングできるようにします。

相互作用や小さな入力値のない膨大な関数は、ほとんどマルチプロセッシングのために呼び出します。そして、すぐにMPIに切り替える(ほぼ無制限のスケーリングで)ことはあまり難しくありません。

技術面では、LinuxのAFAIKコンテキストスイッチは、OSXやHurd(Machマイクロカーネル)の方がはるかに安価ですが、非常に高価です(カーネルスペースの多いモノリシックカーネル)。それはかもしれません Linuxではなく、OSXで見るシステム時間の膨大な量を説明します。