この章では、OpenMP の入れ子並列処理について説明します。
OpenMP は並列実行の fork-join モデルを使用しています。スレッドは並列構文を検出すると、自身を含めほかのスレッドとチームを構成します (ほかのスレッドがまったくないこともあります)。並列構文を検出したスレッドは、このチームのマスタースレッドとなり、チーム内のその他のスレッドは、スレーブスレッドとなります。すべてのスレッドは、並列構文内のコードを実行します。各スレッドは並列構文内での処理を終了すると、その並列構文の最後にある暗黙バリアで待ち状態となります。チーム内のすべてのスレッドがバリアで待ち状態に入れば、スレッドは解放されます。マスタースレッドだけは並列構文の処理が終了したあとも続けてユーザーコードを実行しますが、スレーブスレッドは今度は別のチームを構成するための呼び出しの待ち状態に入ります。
OpenMP での並列領域は、互いに入れ子にすることができます。スレッドが並列領域内で並列構文を検出してチームを作成する際に、入れ子並列処理が無効になっていると、チームに含まれるスレッドは並列構文を検出したスレッドだけとなります。入れ子並列処理が有効になっていれば、複数のスレッドでチームが作成されます。
OpenMP 実行時ライブラリにはスレッドがプールされていて、並列領域内でのスレーブスレッドとして使用されます。あるスレッドが並列構文の検出時に複数のスレッドで構成されるチームを作成する必要がある場合は、そのスレッドは、最初にプールを調べてアイドル状態のスレッドを選択し、自身のチームのスレーブスレッドにします。このとき、充分な数のアイドル状態のスレッドがプールにないと、マスタースレッドが取得できるスレーブスレッドの数は必要な数を満たさないこともあります。チームが並列領域での処理を完了すると、スレーブスレッドはプールに返されます。
入れ子並列処理は、プログラムの実行前にさまざまな環境変数を設定することでその実行を制御できます。
入れ子並列処理は、OMP_NESTED 環境変数を設定するか omp_set_nested() を呼び出すことで有効または無効に設定できます。
3 つのレベルを持つ、入れ子並列構文の例を次に示します。
#include <omp.h> #include <stdio.h> void report_num_threads(int level) { #pragma omp single { printf("Level %d: number of threads in the team - %d\n", level, omp_get_num_threads()); } } int main() { omp_set_dynamic(0); #pragma omp parallel num_threads(2) { report_num_threads(1); #pragma omp parallel num_threads(2) { report_num_threads(2); #pragma omp parallel num_threads(2) { report_num_threads(3); } } } return(0); } |
入れ子並列処理を有効にして、このプログラムをコンパイルおよび実行すると、次のようなソート済みの結果が出力されます。
% setenv OMP_NESTED TRUE % a.out Level 1: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 |
入れ子並列処理を無効にして同じプログラムを実行した場合と比べてみましょう。
% setenv OMP_NESTED FALSE % a.out Level 1: number of threads in the team - 2 Level 2: number of threads in the team - 1 Level 3: number of threads in the team - 1 Level 2: number of threads in the team - 1 Level 3: number of threads in the team - 1 |
OpenMP 実行時ライブラリにはスレッドがプールされていて、並列領域内でのスレーブスレッドとして使用されます。SUNW_MP_MAX_POOL_THREADS 環境変数を設定することで、プールに保存しておけるスレッドの最大数を制限できます。この変数のデフォルト値は 1023 です。
プールにあるのは、実行時ライブラリが作成した非ユーザースレッドだけです。最初のスレッドやユーザーのプログラムが明示的に作成したスレッドは含まれません。この環境変数をゼロに設定すると、スレッドのプールは空になり、すべての並列領域は 1 つのスレッドによって実行されます。
次の例は、プールに十分な数のスレッドがない場合には、並列領域が取得できるスレッドの数が少なくなることを示す例です。コードは、上記の例と同じです。アクティブ化されるすべての並列領域に必要な同時スレッドの数は、8 個です。プールには、少なくとも 7 個のスレッドが含まれている必要があります。ここで SUNW_MP_MAX_POOL_THREADS 変数を 5 に設定すると、もっとも内側の入れ子にある 4 つの並列領域のうち、2 つは必要な数のスレーブスレッドを取得できない場合があります。実行結果はさまざまですが、1 つの例を見てみましょう。
% setenv OMP_NESTED TRUE % setenv SUNW_MP_MAX_POOL_THREADS 5 % a.out Level 1: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 1 Level 3: number of threads in the team - 1 |
環境変数 SUNW_MP_MAX_NESTED_LEVELS は、複数のスレッドを必要とする入れ子になった有効な並列領域の最大の深さを制限します。
この環境変数で指定した数を超える有効な入れ子を持つ有効な並列領域は、1 つのスレッドによって実行されます。IF 節がないか、IF 節の評価が true となる場合は、並列領域は有効であると見なされます。有効な入れ子レベルのデフォルトの最大数は 4 です。
次に、4 重の入れ子になった並列領域のコードの例を示します。SUNW_MP_MAX_NESTED_LEVELS が 2 に設定されると、3 番目と 4 番目の深さにある入れ子並列領域は 1 つのスレッドによって実行されます。
#include <omp.h> #include <stdio.h> #define DEPTH 5 void report_num_threads(int level) { #pragma omp single { printf("Level %d: number of threads in the team - %d\n", level, omp_get_num_threads()); } } void nested(int depth) { if (depth == DEPTH) return; #pragma omp parallel num_threads(2) { report_num_threads(depth); nested(depth+1); } } int main() { omp_set_dynamic(0); omp_set_nested(1); nested(1); return(0); } |
入れ子の深さの最大数を 4 に設定してこのプログラムをコンパイル、実行すると、次のような結果が出力されます。実際の結果は、OS がどのようにスレッドをスケジューリングしているかによって異なります。
% setenv SUNW_MP_MAX_NESTED_LEVELS 4 % a.out |sort Level 1: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 3: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 Level 4: number of threads in the team - 2 |
入れ子の深さを 2 に設定して実行した場合の結果は次のとおりです。
% setenv SUNW_MP_MAX_NESTED_LEVELS 2 % a.out |sort Level 1: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 2: number of threads in the team - 2 Level 3: number of threads in the team - 1 Level 3: number of threads in the team - 1 Level 3: number of threads in the team - 1 Level 3: number of threads in the team - 1 Level 4: number of threads in the team - 1 Level 4: number of threads in the team - 1 Level 4: number of threads in the team - 1 Level 4: number of threads in the team - 1 |
この例は、可能性のある結果の一部のみを示しています。実際の結果は、OS がどのようにスレッドをスケジューリングしているかによって異なります。
ここでは、入れ子並列領域内で次の OpenMP ルーチンを呼び出す実行について説明します。
- omp_set_num_threads() - omp_get_max_threads() - omp_set_dynamic() - omp_get_dynamic() - omp_set_nested() - omp_get_nested()
「set」呼び出しは、呼び出しスレッドが検出した並列領域と同じレベルまたはその内側で入れ子になっている、呼び出し以降の並列領域に対してのみ有効です。ほかのスレッドが検出した並列領域には無効です。
「get」呼び出しは、呼び出しスレッドが設定した値を返します。スレッドが並列領域の実行時にチームのマスターになる場合は、チームのほかのすべてのメンバーはマスタースレッドが持つ値を継承します。マスタースレッドが入れ子並列領域を終了し、その領域を取り囲む並列領域の実行を続ける場合、そのスレッドの値は、入れ子並列領域を実行する直前に、取り囲んでいる並列領域内での値に戻ります。
#include <stdio.h> #include <omp.h> int main() { omp_set_nested(1); omp_set_dynamic(0); #pragma omp parallel num_threads(2) { if (omp_get_thread_num() == 0) omp_set_num_threads(4); /* line A */ else omp_set_num_threads(6); /* line B */ /* The following statement will print out * * 0: 2 4 * 1: 2 6 * * omp_get_num_threads() returns the number * of the threads in the team, so it is * the same for the two threads in the team. */ printf("%d: %d %d\n", omp_get_thread_num(), omp_get_num_threads(), omp_get_max_threads()); /* Two inner parallel regions will be created * one with a team of 4 threads, and the other * with a team of 6 threads. */ #pragma omp parallel { #pragma omp master { /* The following statement will print out * * Inner: 4 * Inner: 6 */ printf("Inner: %d\n", omp_get_num_threads()); } omp_set_num_threads(7); /* line C */ } |
/* Again two inner parallel regions will be created, * one with a team of 4 threads, and the other * with a team of 6 threads. * * The omp_set_num_threads(7) call at line C * has no effect here, since it affects only * parallel regions at the same or inner nesting * level as line C. */ #pragma omp parallel { printf("count me.\n"); } } return(0); } |
このプログラムをコンパイル、実行すると次のような結果が出力されます。
% a.out 0: 2 4 Inner: 4 1: 2 6 Inner: 6 count me. count me. count me. count me. count me. count me. count me. count me. count me. count me. |
並列領域を入れ子にすると、計算で使用できるスレッドの数を簡単に増やすことができます。
たとえば、並列性に 2 つのレベルがあり、各レベルでの並列性の度合いが 2 であるプログラムがあるとしましょう。また、システムには CPU が 4 個搭載され、このプログラムの実行速度を上げるために、これら 4 つの CPU をすべて使用するものとします。どの段階であったとしても単に並列処理にしただけでは、使用する CPU は 2 つに留まります。入れ子によって両方の段階で処理が並列化されます。
並列領域を入れ子にするだけでは、スレッドばかりが増えてシステムへの要求が過剰になります。システムに対する過剰な処理要求を防ぐために、SUNW_MP_MAX_POOL_THREADS および SUNW_MP_MAX_NESTED_LEVELS 環境変数を適切に設定し、使用するスレッドの数を制限します。
入れ子になった並列領域を作成すると負荷がかかります。外側の入れ子でも充分な並列処理が実行されていて、負荷が平均に分散されていれば、現在の処理より内側に入れ子の並列領域を作成するよりは、外側の入れ子で全スレッドを使用する方が一般的には効率的です。
たとえば、2 段階の並列処理となるプログラムがあったとします。外側の処理は 4 つの並列処理となっていて、負荷は平均に分散されています。システムには 4 つの CPU があり、すべての CPU を使用してプログラムの実行を高速化したいとします。この場合は、外側の並列処理で 4 つのスレッドのうち 2 つだけを使い、かつ、そのスレーブスレッドとして内側の並列処理で 2 つのスレッドを使うよりは、外側の並列処理で 4 つのスレッドすべてを使用した方が優れたパフォーマンスを得ることができます。