並列セクションのスレッドの実行時間が非常に異なるという点で、OMP スレッド化に問題があります。Linux クラスターで実行しています。コードは純粋な OpenMP コードです。たとえば、MPI コードが混在していません。C++ で記述されています。私は gcc 4.5.3 を使用しているため、OpenMP 3.0 を使用しています。コンパイル最適化レベル 2 を使用しています。すなわち、-O2。最初にコードを示し、次にそれから生成されたデータを示します。マップのキーをループしたいので、まずキーをベクトル vec_keys にコピーし、vec_keys の要素に対して並列 for ループを実行します。OMP で並列化された for ループがあります。処理する必要がある「ノード」またはエントリは 800,000 あります。並列 for ループ内には 3 つの「コマンド」があります。
キーに基づいてマップ イテレータを取得します。次の行を参照してください: node = vec_keys.at(itime);
ステップ 1 の反復子を使用して、メソッドが呼び出される C++ オブジェクトへのポインターを取得します。次の行を参照してください: p_nim = p_dmb->getModel(node);
ステップ 2 でマップから取得したオブジェクトでメソッドを呼び出します。次のステートメントを参照してください
。
手順 2 では、マップ エントリが取得されますが、書き込まれないことに注意してください。ステップ 3 では、マップのコンテンツが変更されていますが、間接的に変更されています。つまり、マップ キーは変更されません。(マップ エントリ内の) 値は、ヒープ上でインスタンス化されたプリミティブ配列へのポインターです。したがって、値ポインターを変更しないことで、プリミティブ配列の内容を変更できます。要点は、私がマップで作業していて、各キーが OMP の for ループで 1 回呼び出され、競合状態や一貫性のないメモリがないことです。1、2、4、および 8 スレッドで何度も実行しましたが、出力は常に正しいです。上記の手順 1 と 2 の操作は、すべてのマップ キーで同じです。ステップ 3 のみ異なる場合があります。コードは次のとおりです。
#pragma omp parallel num_threads(numSpecOmpThreads) \
private(itime,node,index,modelId,isStateUpdate,tid, \
b1time, \
e1time) \
shared(numkeys,vec_keys,p_graph,p_cp,ifsm,p_np,p_dmb, \
p_ep,p_fg,newStateNp,day,iteration,fsmId, \
fakeMpiRankOpenMp,cout,outStream, \
startWtime,endWtime,counter, \
sinnertime,einnertime, \
dt1time, \
dt2time, \
dt3time, \
countChange) \
default(none)
{
// Any variable in here is private.
tid=omp_get_thread_num();
NodeInteractModel02IF *p_nim=0;
startWtime[tid] = omp_get_wtime();
if (tid==0) {
gettimeofday(&sinnertime,0);
}
#pragma omp for nowait
for (itime=0; itime<numkeys; ++itime) {
++(counter[tid]);
// node is a tail, or owned node.
// This is step 1.
gettimeofday(&b1time,0);
node = vec_keys.at(itime);
gettimeofday(&e1time,0);
dt1time[tid] += (1000000 * (e1time.tv_sec - b1time.tv_sec) +
e1time.tv_usec - b1time.tv_usec);
// This is step 2.
gettimeofday(&b1time,0);
p_nim = p_dmb->getModel(node);
gettimeofday(&e1time,0);
dt2time[tid] += (1000000 * (e1time.tv_sec - b1time.tv_sec) +
e1time.tv_usec - b1time.tv_usec);
// This is step 3.
gettimeofday(&b1time,0);
isStateUpdate = p_nim->computeNextState(lots of args);
gettimeofday(&e1time,0);
dt3time[tid] += (1000000 * (e1time.tv_sec - b1time.tv_sec) +
e1time.tv_usec - b1time.tv_usec);
if (isStateUpdate) {
++(countChange[tid]);
}
} // End FOR over vector of owned nodes.
endWtime[tid] = omp_get_wtime();
if (tid==0) {
gettimeofday(&einnertime,0);
}
} // End pragma on OMP parallel.
さて、問題です。具体的な例として、8 スレッドの実行を取り上げます。実行結果を以下に示します。これらは典型的なものです。dt1 は、上記の最初のステップを実行する累積時間 (秒単位) です。dt2 は、上記の 2 番目のステップを実行する累積時間です。dt3 は、上記のステップ 3 を実行する累積時間です。cum= dt1+dt2+dt3. countChange は、ステップ 3 で変更された「ノード」の数です。各スレッドに 1 つずつ、8 行のデータがあります (tid=0 はデータの最初の行、…、tid=7 は最後の行です)。この実行には 800,000 個の「ノード」があるため、最大で 8 x 100,000 = 800,000 個の countChanges が存在する可能性があります。合計80万ノードのうち、各スレッドが10万ノードを処理していることを確認しています。したがって、処理するノードの数に関しては、作業は各スレッドで同じです。ただし、後述するように、
+++++++++++++++++++++++++++
dt1 dt2 dt3 cum (s) countChange
0.013292 0.041117 3.10149 3.1559 15
0.009705 0.041273 3.17969 3.23067 21
0.009907 0.040998 3.29188 3.34279 16
0.009905 0.040169 3.38807 3.43814 26
0.023467 0.039489 0.198228 0.261184 100000
0.023945 0.038114 0.187334 0.249393 100000
0.023648 0.042231 0.197294 0.263173 100000
0.021285 0.046682 0.219039 0.287006 100000
予想どおり、dt1 は dt2 より小さくなっています。ステップ 3 には計算が含まれるため、予想どおり、どちらも dt3 より小さくなっています。ただし、dt3 値の問題に注意してください。それらは 1 桁以上異なり、2 つのグループにまとめられます。1 つのグループには dt3 ~ 3.2 があり、もう 1 つのグループには dt3 ~ 0.19 があります。さらに、実行が最も速いスレッドは、最も多くのことを行うスレッドです。仕事; 後者の 4 つのスレッドはそれぞれ 100,000 の値すべてを変更しますが、最初の 4 つのスレッドは 15 ~ 26 の値の間で変更します (これは明らかに 100,000 より小さいオーダーです)。ノードが変更されると計算が増えるため、後者の 4 つのスレッドはより多くの作業を行います。さらに、私が実行しているマシンは、2 ノード、ノードあたり 4 コアの計算ノードです。マスター スレッドは tid=0 であり、どちらかといえば時間が短いと予想されますが、時間の長いグループに属しています。また、シングル スレッド コードは cum ~ 11.3 秒を生成します。さて、11.3/8 = 1.41 秒です。
コードはこのループを何百万回も実行するため、理想的な時間 (1.41 秒) と最大測定時間 (上記の 3.44 秒) との差は非常に大きく、過度に見えます。
さらに、上記の例を 8 スレッドではなく 4 スレッドで実行すると、最初の 2 つのスレッドの時間が過剰になり、後の 2 つのスレッドの時間が速くなります。次の 4 スレッド データを参照してください。
+++++++++++++++++++++++++++
dt1 dt2 dt3 cum (s) countChange
0.023794 0.073054 5.41201 5.50886 36
0.018677 0.072956 5.77536 5.86699 42
0.027368 0.066898 0.341455 0.435721 200000
0.026892 0.076005 0.363742 0.466639 200000
繰り返しますが、最初の 2 つのスレッドと最後の 2 つのスレッドの間の差異は、時間的には 1 桁です (~5.5 対 ~0.4)。繰り返しになりますが、実行速度が最も速いスレッドが最も多くの作業を行います。
サンプルの 2 スレッド データを次に示します。2 番目のスレッドはより多くの作業を行います (400,000 ノードを変更しますが、最初のスレッドは 78 ノードのみを変更します) が、桁違いに速く実行されます (10.8 対 0.8)。+++++++++++++++++++++++++++
dt1 dt2 dt3 cum (s) countChange
0.025298 0.144209 10.6269 10.7964 78
0.019307 0.126661 0.619432 0.7654 400000
OpenMP のみを使用して、また OpenMP + MPI コードを組み合わせてこの実験を何度も繰り返しましたが、毎回同じ結果が得られました (もちろん、値は少し調整されていますが、傾向は同じです)。スレッドの最初の半分 (最小の tid を持つスレッド) は、最も長く実行され、作業が少なくなります。また、gcc 4.7.0 を使用しているため、OpenMP 3.1 でも同じ結果が得られます。
これらのスレッドの実行時間に大きな違いがある理由と、それをなくすために何ができるかについて、助けていただければ幸いです。