OpenMPプログラミング - for文の並列実行

いちごパック > OpenMPの解説 > OpenMPプログラミング - for文の並列実行

for文の分割実行

同じことを繰り返し処理するfor文は、並列実行に向いています。 OpenMPでは#pragma omp parallelがあると同じコードが並列に実行されますが、 スレッドごとにループの区間を切り替えれば、繰り返し処理を複数のスレッドで分担できます。 例として、次の6回のループを実行するソースコードを考えます。
#include <iostream>
#include <omp.h>

int main()
{
    {
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            std::cout << "ichigoloop " << ichigoloop
                      << " thread " << omp_get_thread_num() << std::endl;
        }
    }

    return 0;
}
このコードは1つのスレッドで実行されますので、その出力は次のようになります。
ichigoloop 0 thread 0
ichigoloop 1 thread 0
ichigoloop 2 thread 0
ichigoloop 3 thread 0
ichigoloop 4 thread 0
ichigoloop 5 thread 0

このソースコードは、6回のループをスレッドに配分することで並列実行できます。 スレッド番号はomp_get_thread_num()、全スレッド数はomp_get_num_threads()で取得できますから、 ソースコードを次のように書き換えれば、6回のループは並列に実行できます。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        int l_begin = omp_get_thread_num() * 6 / omp_get_num_threads();
        int l_end = (omp_get_thread_num()+1) * 6 / omp_get_num_threads();
        for ( int ichigoloop = l_begin; ichigoloop < l_end; ichigoloop++ ) {
            #pragma omp critical(crit_cout)
            {
                std::cout << "ichigoloop " << ichigoloop
                          << " thread " << omp_get_thread_num() << std::endl;
            }
        }
        #pragma omp barrier
    }

    return 0;
}
このソースコードの出力は次のようになります。
ichigoloop 0 thread 0
ichigoloop 1 thread 1
ichigoloop 3 thread 2
ichigoloop 4 thread 3
ichigoloop 5 thread 3
ichigoloop 2 thread 1
ループによって別々のスレッド番号が割り当てられ、並列実行されたことが確認できます。

OpenMPによるforループのサポート

ループを手作業でスレッドに配分しなくても並列化できるように、 OpenMPは#pragma omp forを提供しています。
#pragma omp forは、並列実行中の範囲に置かれた場合にのみ、 直後にあるforループを各スレッドに配分する機能を持ちます。 omp_get_num_threads()が1を返す場合は、1つのスレッドで実行します。 #pragma omp forが新しいスレッドを作ることはありませんので、 並列実行したい場合は#pragma omp parallelが有効な範囲に#pragma omp forを置きます。
#pragma omp forを用いて先ほどのソースコードを書き換えると、次のようになります。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            #pragma omp critical(crit_cout)
            {
                std::cout << "ichigoloop " << ichigoloop
                          << " thread " << omp_get_thread_num() << std::endl;
            }
        }
        #pragma omp barrier
    }

    return 0;
}
OpenMPに対応したコンパイラでビルドし、実行した結果の1例を示します。
ichigoloop 0 thread 0
ichigoloop 1 thread 0
ichigoloop 4 thread 2
ichigoloop 5 thread 3
ichigoloop 2 thread 1
ichigoloop 3 thread 1
すでに説明してきましたが、std::cout出力の競合を防ぐためにはクリティカルセクションが必要になります。 試しに#pragma omp critical(crit_cout)を入れない、次のソースコードを作成してみます。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            std::cout << "ichigoloop " << ichigoloop
                      << " thread " << omp_get_thread_num() << std::endl;
        }
        #pragma omp barrier
    }

    return 0;
}
このコードをOpenMPに対応したコンパイラでビルドし実行すると、 例えば次のように複数のスレッドの出力が混ざった出力になることが確認できます。
ichigoloop 0ichigoloop 2ichigoloop 4 thread  thread ichigoloop 25
1 thread 3

 thread 0ichigoloop 3 thread 1

ichigoloop 1 thread 0
このようなリソースの競合にクリティカルセクションを挿入しなくても、 偶然うまく動いているようにみえることがあります。 parallelの対象範囲でコードを書くときは、常にリソースの競合がないか確認し、 必要であればクリティカルセクションを挿入するように気を付けたほうが良いでしょう。

forに対するバリアとnowait

#pragma omp forを使うと、forが終わる時点にバリアが自動的に挿入されます。 例として、次のコードを考えます。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            #pragma omp critical(crit_cout)
            {
                std::cout << "ichigoloop " << ichigoloop
                          << " thread " << omp_get_thread_num() << std::endl;
            }
        }

        #pragma omp critical(crit_cout)
        {
            std::cout << "ichigothread " << omp_get_thread_num() << std::endl;
        }
        #pragma omp barrier
    }

    return 0;
}
このコードではfor文の最後にバリアが挿入されますので、 次の出力例のように、すべてのfor文が完了するまでichigothreadが出力されることはありません。
ichigoloop 0 thread 0
ichigoloop 1 thread 0
ichigoloop 4 thread 2
ichigoloop 5 thread 3
ichigoloop 2 thread 1
ichigoloop 3 thread 1
ichigothread 1
ichigothread 2
ichigothread 0
ichigothread 3
このバリアが必要ない場合は、#pragma omp for nowaitのようにnowait指示を加えます。 先のコードであれば次のようになります。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for nowait
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            #pragma omp critical(crit_cout)
            {
                std::cout << "ichigoloop " << ichigoloop
                          << " thread " << omp_get_thread_num() << std::endl;
            }
        }

        #pragma omp critical(crit_cout)
        {
            std::cout << "ichigothread " << omp_get_thread_num() << std::endl;
        }
        #pragma omp barrier
    }

    return 0;
}
バリアがなくなったため、次の出力例のように、 for文の完了を待つことなくichigothreadが出力されるようになります。
ichigoloop 0 thread 0
ichigoloop 1 thread 0
ichigothread 0
ichigoloop 4 thread 2
ichigoloop 5 thread 3
ichigoloop 2 thread 1
ichigoloop 3 thread 1
ichigothread 1
ichigothread 3
ichigothread 2

for文の実行順序制御

#pragma omp forを使うと、対象とするfor文は任意の順序で実行されます。 例えば次のコードを考えてみます。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for nowait
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            #pragma omp critical(crit_cout)
            {
                std::cout << "ichigoloop " << ichigoloop << " thread " << omp_get_thread_num() << std::endl;
            }
        }
        #pragma omp barrier
    }

    return 0;
}
この場合、for文の実行順序は決まっておらず、実行するたびに別々の順序で実行されます。 以下に出力の1例を示します。
ichigoloop 0 thread 0
ichigoloop 4 thread 2
ichigoloop 2 thread 1
ichigoloop 1 thread 0
ichigoloop 3 thread 1
ichigoloop 5 thread 3
しかしながら、for文の順序通りの実行が必要なこともあります。
#pragma omp for orderedと#pragma omp orderedは、for文の実行順序を制御する機能を提供します。 この2つの#pragmaは、必ずセットで使います。
#pragma omp for orderedは、for文の直前に置きます。 この#pragmaは、for文の中に順序通り実行すべき部分が存在することを指示します。
#pragma omp orderedは、#pragma omp for orderedの対象となるfor文の中に置きます。 この#pragmaの対象領域は、for文のループ順序の通りに実行されます。
先のソースコードを、この2つの#pragmaを使って書き換えた例を示します。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for nowait ordered
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            #pragma omp ordered
            {
                #pragma omp critical(crit_cout)
                {
                    std::cout << "ichigoloop " << ichigoloop << " thread " << omp_get_thread_num() << std::endl;
                }
            }
        }
        #pragma omp barrier
    }

    return 0;
}
このソースコードは、ichigoloopが0から開始し1ずつ増える順序で、 std::coutへの出力を行う部分を実行するように並列化されます。 以下にその出力を示します。ichigoloopの順序がfor文の順序と一致していることが確認できます。
ichigoloop 0 thread 0
ichigoloop 1 thread 0
ichigoloop 2 thread 1
ichigoloop 3 thread 1
ichigoloop 4 thread 2
ichigoloop 5 thread 3
#pragma omp orderedがない部分は任意の順序で実行されます。 そのため、次のソースコードのように#pragma omp orderedを入れ忘れると、任意の順序で実行されます。
#include <iostream>
#include <omp.h>

int main()
{
    #pragma omp parallel
    {
        #pragma omp for nowait ordered
        for ( int ichigoloop = 0; ichigoloop < 6; ichigoloop++ ) {
            //#pragma omp ordered
            #pragma omp critical(crit_cout)
            {
                std::cout << "ichigoloop " << ichigoloop << " thread " << omp_get_thread_num() << std::endl;
            }
        }
        #pragma omp barrier
    }

    return 0;
}
以下にその出力を示します。ichigoloopの順序とは無関係な順序で実行されたことがわかります。
ichigoloop 0 thread 0
ichigoloop 1 thread 0
ichigoloop 2 thread 1
ichigoloop 5 thread 3
ichigoloop 4 thread 2
ichigoloop 3 thread 1
orderedがなくても偶然、順序通り実行されることがあります。 そのため、orderedを入れ忘れても、テスト中にその不具合を見つけられないことがあります。 forの中で#pragma omp orderedを忘れないようにご注意ください。

関連ページ

  • OpenMPの解説 目次
  • 前の項目: 並列実行中の1スレッド実行
  • 次の項目: 複数のソースコードの並列実行