違いは、最初のブロックでは実際にはタスクを作成していないということです。これは、ブロック自体が (構文的にも字句的にも) アクティブな並列領域内にネストされていないためです。2 番目のブロックでは、task
コンストラクトが領域内に構文的にネストされ、parallel
実行時に領域がアクティブになった場合に明示的なタスクをキューに入れます (アクティブな並列領域とは、複数のスレッドのチームで実行される領域です)。字句の入れ子はそれほど明白ではありません。次の例に注意してください。
void foo(void)
{
int i;
for (i = 0; i < 10; i++)
#pragma omp task
bar();
}
int main(void)
{
foo();
#pragma omp parallel num_threads(4)
{
#pragma omp single
foo();
}
return 0;
}
への最初の呼び出しfoo()
は、並列領域の外で発生します。したがって、task
ディレクティブは (ほとんど) 何もせず、すべての呼び出しがbar()
連続して発生します。の 2 番目の呼び出しfoo()
は、並列領域内から行われるため、新しいタスクが 内で生成されfoo()
ます。句によってスレッド数が固定されているため、parallel
リージョンはアクティブです。4
num_threads(4)
OpenMP ディレクティブのこの異なる動作は設計機能です。主なアイデアは、シリアルとパラレルの両方で実行できるコードを記述できるようにすることです。
それでも、task
コンストラクトが存在すると、foo()
コード変換が行われます。たとえばfoo()
、次のように変換されます。
void foo_omp_fn_1(void *omp_data)
{
bar();
}
void foo(void)
{
int i;
for (i = 0; i < 10; i++)
OMP_make_task(foo_omp_fn_1, NULL);
}
これは、最初の引数として指定された関数への呼び出しをキューに入れる、 OpenMPOMP_make_task()
サポート ライブラリの架空の (公開されていない) 関数です。OMP_make_task()
アクティブな並列領域の外で動作することが検出された場合は、foo_omp_fn_1()
代わりに呼び出すだけです。bar()
これにより、シリアルの場合の呼び出しにいくらかのオーバーヘッドが追加されます。の代わりにmain -> foo -> bar
、呼び出しは次のようになりmain -> foo -> OMP_make_task -> foo_omp_fn_1 -> bar
ます。これは、シリアル コードの実行が遅くなることを意味します。
これは、ワークシェアリング ディレクティブでさらに明確に示されます。
void foo(void)
{
int i;
#pragma omp for
for (i = 0; i < 12; i++)
bar();
}
int main(void)
{
foo();
#pragma omp parallel num_threads(4)
{
foo();
}
return 0;
}
への最初の呼び出しfoo()
は、ループをシリアルに実行します。2 番目の呼び出しでは、12 回の反復が 4 つのスレッドに分散されます。つまり、各スレッドは 3 回の反復しか実行しません。繰り返しになりますが、これを実現するために何らかのコード変換マジックが使用されており、シリアル ループは に no#pragma omp for
が存在する場合よりも遅く実行されfoo()
ます。
ここでの教訓は、実際には必要のない場所に OpenMP コンストラクトを追加しないことです。