11

大学のコースの一環として、OpenMP の使用方法を学び始めました。ラボの演習として、並列化する必要があるシリアル プログラムが与えられました。

False Sharingの危険性を最初に認識したことの 1 つは、特に for ループで並列に配列を更新する場合です。

ただし、False Sharing を発生させずに、次のコード スニペットを並列化可能なタスクに変換するのは難しいと思います。

int ii,kk;

double *uk = malloc(sizeof(double) * NX);
double *ukp1 = malloc(sizeof(double) * NX);
double *temp;

double dx = 1.0/(double)NX;
double dt = 0.5*dx*dx;

// Initialise both arrays with values
init(uk, ukp1);

for(kk=0; kk<NSTEPS; kk++) {
   for(ii=1; ii<NX-1; ii++) {
      ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
   }

   temp = ukp1;
   ukp1 = uk;
   uk = temp;
   printValues(uk,kk);
}

私の最初の反応は、ukp1を共有してみることでした:

for(kk=0; kk<NSTEPS; kk++) {
   #pragma omp parallel for shared(ukp1)
   for(ii=1; ii<NX-1; ii++) {
      ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
    }

   temp = ukp1;
   ukp1 = uk;
   uk = temp;
   printValues(uk,kk);
}

しかし、これは明らかにシリアル バージョンと比較して大幅な速度低下を示しています。明らかな理由は、 ukp1への書き込み操作中に False 共有が発生していることです。

リダクション句を使用できるのではないかという印象を受けましたが、これは配列では使用できないことがすぐにわかりました。

このコードを並列化してランタイムを改善するために使用できるものはありますか? 聞いたことのない使用できる条項はありますか? それとも、適切な並列化を可能にするためにコードを再構築する必要があるようなタスクですか?

あらゆる形式の入力をお待ちしております。

EDIT:私のコードに間違いがあったと指摘されました。私がローカルに持っているコードは正しいです。間違って編集しただけです (コードの構造が変更されました)。混乱して申し訳ありません!

EDIT2

@Sergeyが私に指摘してくれたいくつかの情報は、私が役に立つと感じています:

  • uk または ukp1 をプライベートに設定すると、基本的に共有に設定するのと同じ効果があります。これは、両方とも同じメモリ位置へのポインターであるためです。

  • 静的スケジューリングを使用すると、理論的には役立つはずですが、私はまだ同じ速度低下を経験しています。また、静的スケジューリングは、この問題を修正する最も移植性の高い方法ではないと感じています。

4

2 に答える 2

13

最初に最適化について話しているので、最初に次のことを行います。

定数をマクロとして定義すると、コンパイラによる最適化が向上します。

#define dx (1.0/(double)NX)
#define dt (0.5*dx*dx)

OpenMP* を使用する場合、変数のデフォルトの共有ルールは です。ただし、並列セクション内で必要なすべての変数を手動でsharedに設定して有効にすることをお勧めします。noneこれにより、競合を確実に回避できます。

#pragma omp parallel for default(none) shared(ukp1, uk)

またukp1、 orukを共有ステータスに設定すると、ポインターとして宣言されているため、ポインターは並列セクションにのみ渡されます。そのため、それらのメモリはまだ共有されています。

最後に、フラッシュの共有を避けるために、スレッド間で共有されるキャッシュ ラインをできる限り少なくする必要があります。読み取り専用キャッシュライン (つまりuk、あなたの場合) は関係ありません。それらはすべてのスレッドに存在できますが、書き込みキャッシュラインはスレッドごとに存在するukp1必要があります。現在、キャッシュ ラインの長さは通常 64 バイトです。したがって、1 つのキャッシュ ラインは 8double秒に収まります。したがって、スレッドごとに少なくとも 8 回の繰り返しのチャンクを割り当てたいとします。

#pragma omp parallel for default(none) shared(ukp1, uk) schedule(static,8)

チャンクごとにコード 8 回の反復をデプロイし、内側のループに表示する必要があります。

for(kk=0; kk<NSTEPS; kk++) {
   #pragma omp parallel for default(none) shared(ukp1, uk) schedule(static,8)
   for(ii=1; ii<NX-1; ii++) {
      ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
   }
   // Swap pointers for the next time step
   temp = ukp1;
   ukp1 = uk;
   uk   = temp;
}

実際には、データのサイズによっては、さらに大きなチャンク サイズを割り当てたい場合があります。私は使用する傾向があります0x1000- ほとんどのシステムでは、ページ全体に収まることさえあります (ページが整列されていると仮定します)。

編集:

これが実際に効果を発揮するには、メモリを正しく配置する必要があります。index から開始している1ので、次のようになります。

 double *uk = memalign(0x40 , sizeof(double) * (NX + 8));
 double *ukp1 = memalign(0x40 , sizeof(double) * (NX + 8));
 uk += 7;
 ukp1 += 7;

ukp1[1]これで、キャッシュラインが整列されました。チャンクサイズを大きくすると役立つ場合がありますがNX > 100000、最初から並列化することを計画していない限り、あまり意味がありません。

各反復で並列セクションを再起動すると、かなりのオーバーヘッドが発生することに注意する必要があります。それを制御するには、単純な OpenMP を超えてスケ​​ジューリングをやり直す必要があります。

于 2013-10-09T17:56:44.247 に答える