68

私はC++を使用してOpenMPを使い始めました。

2つの質問があります:

  1. #pragma omp for scheduleですか?
  2. dynamicとの違いは何staticですか?

例を挙げて説明してください。

4

3 に答える 3

145

それ以来、他の人がほとんどの質問に答えていますが、特定のスケジューリングタイプが他のタイプよりも適しているいくつかの特定のケースを指摘したいと思います。スケジュールは、ループの反復をスレッド間で分割する方法を制御します。適切なスケジュールを選択すると、アプリケーションの速度に大きな影響を与える可能性があります。

staticスケジュールとは、反復ブロックがラウンドロビン方式で実行スレッドに静的にマップされることを意味します。静的スケジューリングの良い点は、OpenMPランタイムが、同じ反復回数の2つの別々のループがあり、静的スケジューリングを使用して同じ数のスレッドでそれらを実行する場合、各スレッドがまったく同じ反復範囲を受け取ることを保証することです( s)両方の並列領域。これはNUMAシステムでは非常に重要です。最初のループでメモリに触れると、実行中のスレッドがあったNUMAノードに常駐します。次に、2番目のループでは、同じNUMAノードに存在するため、同じスレッドが同じメモリ位置に高速にアクセスできます。

ノード0とノード1の2つのNUMAノードがあるとします。たとえば、両方のソケットに4コアCPUを搭載した2ソケットのIntelNehalemボードです。次に、スレッド0、1、2、および3はノード0に常駐し、スレッド4、5、6、および7はノード1に常駐します。

|             | core 0 | thread 0 |
| socket 0    | core 1 | thread 1 |
| NUMA node 0 | core 2 | thread 2 |
|             | core 3 | thread 3 |

|             | core 4 | thread 4 |
| socket 1    | core 5 | thread 5 |
| NUMA node 1 | core 6 | thread 6 |
|             | core 7 | thread 7 |

各コアは各NUMAノードからメモリにアクセスできますが、リモートアクセスはローカルノードアクセスよりも低速です(Intelでは1.5倍から1.9倍低速)。次のようなものを実行します。

char *a = (char *)malloc(8*4096);

#pragma omp parallel for schedule(static,1) num_threads(8)
for (int i = 0; i < 8; i++)
   memset(&a[i*4096], 0, 4096);

この場合の4096バイトは、巨大なページが使用されていない場合のLinuxonx86の1つのメモリページの標準サイズです。このコードは、32KiB配列全体をゼロにしますa。呼び出しはmalloc()仮想アドレス空間を予約するだけですが、実際には物理メモリに「接触」しません(これは、他のバージョンmallocが使用されていない限り、デフォルトの動作です。たとえば、メモリをゼロにするバージョンなどcalloc())。現在、このアレイは連続していますが、仮想メモリ内にのみ存在します。物理メモリでは、半分はソケット0に接続されたメモリに、残りの半分はソケット1に接続されたメモリにあります。これは、さまざまな部分がさまざまなスレッドによってゼロにされ、それらのスレッドがさまざまなコアに存在し、ファーストタッチと呼ばれるものがあるためです。NUMAポリシーは、メモリページが最初にメモリページに「触れた」スレッドが存在するNUMAノードに割り当てられることを意味します。

|             | core 0 | thread 0 | a[0]     ... a[4095]
| socket 0    | core 1 | thread 1 | a[4096]  ... a[8191]
| NUMA node 0 | core 2 | thread 2 | a[8192]  ... a[12287]
|             | core 3 | thread 3 | a[12288] ... a[16383]

|             | core 4 | thread 4 | a[16384] ... a[20479]
| socket 1    | core 5 | thread 5 | a[20480] ... a[24575]
| NUMA node 1 | core 6 | thread 6 | a[24576] ... a[28671]
|             | core 7 | thread 7 | a[28672] ... a[32768]

次に、次のような別のループを実行してみましょう。

#pragma omp parallel for schedule(static,1) num_threads(8)
for (i = 0; i < 8; i++)
   memset(&a[i*4096], 1, 4096);

各スレッドは、すでにマップされている物理メモリにアクセスし、最初のループの場合と同じスレッドからメモリ領域へのマッピングを行います。これは、スレッドがローカルメモリブロックにあるメモリにのみアクセスすることを意味します。これは高速です。

ここで、別のスケジューリング方式が2番目のループに使用されていると想像してくださいschedule(static,2)。これにより、反復スペースが2つの反復のブロックに「切り刻まれ」、合計4つのそのようなブロックが存在します。何が起こるかというと、次のスレッドからメモリ位置へのマッピング(反復番号による)があります。

|             | core 0 | thread 0 | a[0]     ... a[8191]  <- OK, same memory node
| socket 0    | core 1 | thread 1 | a[8192]  ... a[16383] <- OK, same memory node
| NUMA node 0 | core 2 | thread 2 | a[16384] ... a[24575] <- Not OK, remote memory
|             | core 3 | thread 3 | a[24576] ... a[32768] <- Not OK, remote memory

|             | core 4 | thread 4 | <idle>
| socket 1    | core 5 | thread 5 | <idle>
| NUMA node 1 | core 6 | thread 6 | <idle>
|             | core 7 | thread 7 | <idle>

ここで2つの悪いことが起こります。

  • スレッド4から7はアイドル状態のままで、計算機能の半分が失われます。
  • スレッド2と3は非ローカルメモリにアクセスし、終了するのに約2倍の時間がかかります。その間、スレッド0と1はアイドル状態のままになります。

したがって、静的スケジューリングを使用する利点の1つは、メモリアクセスの局所性が向上することです。欠点は、スケジューリングパラメータの選択を誤ると、パフォーマンスが低下する可能性があることです。

dynamicスケジューリングは「先着順」で機能します。同じスレッド数で2回実行すると、簡単に確認できるように、完全に異なる「反復スペース」->「スレッド」マッピングが生成される可能性があります。

$ cat dyn.c
#include <stdio.h>
#include <omp.h>

int main (void)
{
  int i;

  #pragma omp parallel num_threads(8)
  {
    #pragma omp for schedule(dynamic,1)
    for (i = 0; i < 8; i++)
      printf("[1] iter %0d, tid %0d\n", i, omp_get_thread_num());

    #pragma omp for schedule(dynamic,1)
    for (i = 0; i < 8; i++)
      printf("[2] iter %0d, tid %0d\n", i, omp_get_thread_num());
  }

  return 0;
}

$ icc -openmp -o dyn.x dyn.c

$ OMP_NUM_THREADS=8 ./dyn.x | sort
[1] iter 0, tid 2
[1] iter 1, tid 0
[1] iter 2, tid 7
[1] iter 3, tid 3
[1] iter 4, tid 4
[1] iter 5, tid 1
[1] iter 6, tid 6
[1] iter 7, tid 5
[2] iter 0, tid 0
[2] iter 1, tid 2
[2] iter 2, tid 7
[2] iter 3, tid 3
[2] iter 4, tid 6
[2] iter 5, tid 1
[2] iter 6, tid 5
[2] iter 7, tid 4

(代わりにを使用した場合も同じ動作が見gccられます)

staticセクションのサンプルコードがdynamic代わりにスケジューリングで実行された場合、元の場所が保持される可能性は1/70(1.4%)のみであり、リモートアクセスが発生する可能性は69/70(98.6%)です。この事実は見過ごされがちであるため、最適ではないパフォーマンスが達成されます。

staticスケジューリングとスケジューリングのどちらかを選択するもう1つの理由があります。それは、dynamicワークロードバランシングです。各反復が完了するまでの平均時間と大きく異なる場合、静的なケースで高い作業の不均衡が発生する可能性があります。例として、反復を完了する時間が反復数に比例して増加する場合を考えてみましょう。反復スペースが2つのスレッド間で静的に分割されている場合、2番目のスレッドの作業量は最初のスレッドの3倍になるため、計算時間の2/3の間、最初のスレッドはアイドル状態になります。動的スケジュールは追加のオーバーヘッドをもたらしますが、その特定のケースでは、はるかに優れたワークロード分散につながります。特別な種類のdynamicスケジューリングはguided、作業が進むにつれて各タスクにますます小さな反復ブロックが与えられる場所です。

プリコンパイルされたコードはさまざまなプラットフォームで実行できるため、エンドユーザーがスケジューリングを制御できると便利です。そのため、OpenMPは特別なschedule(runtime)句を提供します。スケジューリングでruntimeは、タイプは環境変数のコンテンツから取得されますOMP_SCHEDULE。これにより、アプリケーションを再コンパイルせずにさまざまなスケジューリングタイプをテストでき、エンドユーザーは自分のプラットフォームに合わせて微調整することもできます。

于 2012-06-01T15:10:02.537 に答える
27

誤解は、OpenMPについてのポイントを見逃しているという事実から来ていると思います。一言で言えば、OpenMPを使用すると、並列処理を有効にすることでプログラムをより高速に実行できます。プログラムでは、並列処理はさまざまな方法で有効にできますが、そのうちの1つはスレッドを使用することです。配列があるとします。

[1,2,3,4,5,6,7,8,9,10]

この配列のすべての要素を1つインクリメントします。

使用する場合

#pragma omp for schedule(static, 5)

これは、各スレッドに5つの連続した反復が割り当てられることを意味します。この場合、最初のスレッドは5つの数字を取ります。2つ目は、処理するデータがなくなるか、スレッドの最大数(通常はコアの数に等しい)に達するまで、さらに5を要します。ワークロードの共有は、コンパイル中に行われます。

の場合には

#pragma omp for schedule(dynamic, 5)

作業はスレッド間で共有されますが、この手順は実行時に発生します。したがって、より多くのオーバーヘッドが発生します。2番目のパラメーターは、データのチャンクのサイズを指定します。

OpenMPにあまり詳しくないので、コードがコンパイルされたシステムとは異なる構成のシステムでコンパイルされたコードを実行する場合は、動的型の方が適切であると考えるリスクがあります。

コードを並列化するために使用される手法、前提条件、および制限について説明している次のページをお勧めします

https://computing.llnl.gov/tutorials/parallel_comp/

追加リンク: http:
//en.wikipedia.org/wiki/OpenMPC
でのopenMPの静的スケジュールと動的スケジュールの違い
http://openmp.blogspot.se/

于 2012-06-01T12:48:34.833 に答える
13

ループパーティションスキームは異なります。静的スケジューラは、N個の要素のループをM個のサブセットに分割し、各サブセットには厳密にN/M個の要素が含まれます。

動的アプローチは、サブセットのサイズをその場で計算します。これは、サブセットの計算時間が変化する場合に役立ちます。

計算時間があまり変化しない場合は、静的アプローチを使用する必要があります。

于 2012-06-01T12:26:16.893 に答える