C++を使用してOpenMPで作業を開始しました。
2つの質問があります。
#pragma omp for schedule
とは何ですか?dynamic
とstatic
の違いは何ですか?例を挙げて説明してください。
他の人が質問のほとんどに回答してきましたが、特定のスケジューリングタイプが他のタイプよりも適している特定のケースを指摘したいと思います。スケジュールは、ループの繰り返しがスレッド間でどのように分割されるかを制御します。適切なスケジュールを選択すると、アプリケーションの速度に大きな影響を与える可能性があります。
static
スケジュールは、反復ブロックがラウンドロビン方式で実行スレッドに静的にマッピングされることを意味します。静的スケジューリングの良い点は、OpenMPランタイムが、同じ繰り返し数の2つの別個のループがあり、静的スケジューリングを使用して同じスレッド数でそれらを実行すると、各スレッドがまったく同じ繰り返し範囲を受け取ることを保証することですs)両方の並列領域。これはNUMAシステムでは非常に重要です。最初のループでメモリに触れると、実行中のスレッドがあったNUMAノードに存在します。その後、2番目のループでは、同じスレッドが同じNUMAノードに常駐するため、同じスレッドがより速く同じメモリ位置にアクセスできます。
2つのNUMAノードがあることを想像してください:ノード0とノード1。両方のソケットに4コアCPUを搭載した2ソケットIntel Nehalemボード。次に、スレッド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バイトは、巨大なページが使用されていない場合のx86上のLinuxの1メモリページの標準サイズです。このコードは、32 KiB配列a
全体をゼロにします。 malloc()
呼び出しは仮想アドレス空間を予約するだけですが、実際には物理メモリを「タッチ」しません(これは、他のバージョンのmalloc
を使用しない限りデフォルトの動作です。 calloc()
のように)。現在、この配列は連続していますが、仮想メモリ内のみです。物理メモリでは、その半分はソケット0に接続されたメモリにあり、半分はソケット1に接続されたメモリにあります。これは、異なる部分が異なるスレッドによってゼロにされ、それらのスレッドが異なるコアに存在し、 first touch 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つの悪いことが起こります。
したがって、静的スケジューリングを使用する利点の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
とdynamic
のスケジューリングを選択する別の理由があります-ワークロードバランシング。各反復が完了するまでの平均時間と大幅に異なる場合、静的な場合に高い仕事の不均衡が発生する可能性があります。例として、反復を完了する時間が反復数に比例して増加する場合を考えます。反復スペースが2つのスレッド間で静的に分割される場合、2番目のスレッドは最初のスレッドの3倍の作業を行うため、計算時間の2/3の間、最初のスレッドはアイドル状態になります。動的なスケジュールにより、追加のオーバーヘッドが発生しますが、その特定のケースでは、ワークロードの分散が大幅に改善されます。特別な種類のdynamic
スケジューリングはguided
で、作業の進行に応じて、より小さな反復ブロックが各タスクに与えられます。
プリコンパイルされたコードはさまざまなプラットフォームで実行できるため、エンドユーザーがスケジューリングを制御できると便利です。 OpenMPが特別なschedule(runtime)
句を提供するのはそのためです。 runtime
スケジューリングでは、タイプは環境変数_OMP_SCHEDULE
_のコンテンツから取得されます。これにより、アプリケーションを再コンパイルせずにさまざまなスケジューリングタイプをテストでき、エンドユーザーが自分のプラットフォームに合わせて微調整することもできます。
誤解は、あなたが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/OpenMP
CのopenMPの静的スケジュールと動的スケジュールの違い
http://openmp.blogspot.se/
ループ分割スキームは異なります。静的スケジューラーは、N個の要素にわたるループをM個のサブセットに分割し、各サブセットには厳密にN/M個の要素が含まれます。
動的アプローチは、サブセットのサイズをその場で計算します。これは、サブセットの計算時間が異なる場合に役立ちます。
計算時間があまり変わらない場合は、静的アプローチを使用する必要があります。