HPC/並列プログラミングポータルでは、HPC(High Performance Computing)プログラミングや並列プログラミングに関する情報を集積・発信しています。

新着トピックス

OpenMPによる並列化

 今回並列化を行ったプログラムでは、このように高速化に成功しているように見えるものの、実はいくつかの問題を含んでいる。まず、今回の実装では最大で2スレッドしか利用しないため、3コア以上を持つシステムではそのパフォーマンスをフルに引き出すことはできない。もし3スレッド以上を同時に実行させたい場合、そのためのコードを別途追加する必要がある。また、それぞれのスレッドに割り当てる仕事量は固定されているため、スレッドに割り当てられた処理が終了したら、そのスレッドはアイドル状態となってしまう。そのほか実装上の問題として、マルチスレッドで処理する部分を別の関数として分離させる必要があるほか、_beginthreadex()関数では実行する関数の引数としてポインタを1つだけしか与えられないため、引数の処理が面倒となる点も挙げられる。

 このような問題は、OpenMPを利用することで大幅に改善が可能だ。OpenMPはC/C++およびFortranで利用できる、並列処理を記述するための言語規格である。OpenMPによる並列化では、並列化を行う個所にプラグマとして命令を埋め込み、対応コンパイラでコンパイルすることで並列化を行う。現在ではVisual C++やGCCなど、多くのコンパイラがこのOpenMPをサポートしており、またWindowsやLinux、各種UNIXからスーパーコンピュータまで、さまざまなプラットフォームで利用できるのも特徴だ。

コラム:OpenMPの歴史

 OpenMPは1997年に発表された標準規格であり、1997年にFortran向けの「OpenMP fortran 1.0」が、1998年にC/C++向けの「OpenMP C/C++ 1.0」がリリースされた。2009年時点の最新版は「OpenMP 3.0」である。OpenMP規格の策定にはインテルやAMDといったCPUメーカーや富士通、ヒューレット・パッカード、IBM、NEC、サン・マイクロシステムズ、マイクロソフトといったハードウェアベンダーやOSベンダーが参加しており、現在ではインテル コンパイラーやVisual C++、GCC、各社が提供するSolarisやAIXなどの商用UNIX向けコンパイラーなどでなんらかのOpenMPサポートが行われている。

OpenMPでプログラムを並列化する

 C/C++でOpenMPを利用する場合「omp.h」をインクルードし、並列化を行いたい個所に「#pragma omp <ディレクティブ>」というプラグマを挿入することで、コンパイラに並列化を行う指示を与える。OpenMPには多数のディレクティブが用意されているが、そのなかで多く利用されるであろうものを表3にまとめておいた。そのほかの詳細については別途文献などを参照してほしい。また、OpenMPを使用するプログラムをコンパイルする際は、Visual C++の場合/openmpオプションを、インテル コンパイラの場合/Qopenmpオプションを、GCCの場合-fopenmpオプションを付ける必要がある。

表3 代表的なOpenMPのディレクティブ
ディレクティブ意味
並列処理ブロックの生成
parallel並列化するブロックを開始する。このプラグマの直後の「{」から「}」までが並列化を行うブロックとなる
forこのプラグマの直後のforループを並列化する。「schedule」オプションでどのようにループを並列化するかも指定可能
sectionsこのプラグマに続くブロック({}で囲まれた個所)内の複数の「section」を並列実行する
section並列実行するブロックを指定する。「sections」と組み合わせて使用する
single並列処理の際、続くブロックを1つのスレッドだけで実行する
変数の扱い
threadprivate(変数)それぞれのスレッドで独立して利用したいグローバル変数を指定する
private(変数)それぞれのスレッドで独立して利用したいローカル変数を指定する
同期に関する構文
master続くブロックをマスタースレッドだけで実行する
critical続くブロックを同時に1つのスレッドだけで実行する
barrier並列実行しているすべてのスレッドがこのプラグマ部分に到着するまで、このプラグマ部分で待機する

 たとえばOpenMPを利用してループを並列化するには、次のようにすればよい。

#pragma omp parallel
#pragma omp for
for (i = 0; i  count; i++) {
    /* 並列実行したい処理をここに記述する */
}

 このように記述された処理を実行する際、OpenMPライブラリは実行環境で同時に実行できるスレッド数などに応じて適切にスレッドを作成し、処理を各スレッドに振り分けて並列に実行させる。もちろん、ユーザーが利用するスレッド数を明示的に指定することも可能だ。このとき、ループの制御変数「i」は各スレッドで独立したものとなる。

 また、下記は複数の処理を同時に実行させる場合の例である。

#pragma omp parallel
#pragma omp sections
{
#pragma omp section
    /* 並列に実行したい処理A */
#pragma omp section
    /* 並列に実行したい処理B */
#pragma omp section
    /* 並列に実行したい処理C */
}
/* すべての処理が完了後に実行する処理 */

 この例の場合、処理A、B、Cがそれぞれ別のスレッドで並列に実行され、すべての処理が完了後に#pragma omp parallel以降の処理が実行される。

 さて、今度はOpenMPを利用し、先のシングルスレッド版メディアンフィルタプログラムを並列化してみよう。OpenMPを使用したループの並列化は非常に簡単で、ループの前に2行のコードを追加するだけである。

 ここで、「#pragma omp parallel private(x, i, j, tmp_array, dx, dy)」という行ではスレッドごとに独立して利用すべき変数を指定しており、また「#pragma omp for」という行では続くforループを並列実行せよ、という指示を与えている。

#pragma omp parallel private(x, i, j, tmp_array, dx, dy)
#pragma omp for
    for(y = 1; y  height - 1; y++) {
        for (x = 1; x  width - 1; x++) {
            /* 対象ピクセルとその近傍8ピクセルのアドレスをソートバッファにコピー*/
            for (j = 0; j  3; j++) {
                for (i = 0; i  3; i++) {
                    tmp_array[3*j + i] = (buf_tmp-array[y-1+j][x-1+i]);
                }
            }
            /* 9ピクセルをソート */
            med_sort(tmp_array, 9);
            /* 中央値となるピクセルの相対位置を取得 */
            dy = get_med_position_y(tmp_array, (buf_tmp-array[y][x]));
            dx = get_med_position_x(tmp_array, (buf_tmp-array[y][x]), width, dy);

            /* 出力バッファの対象ピクセルの値を中央値となるピクセルの値に設定 */
            for (i = 0; i  3; i++) {
                buf_out-array[y][3 * x + i] = buf_in-array[y+dy][3 * (x+dx) + i];
            }
        }
    }

 このように、OpenMPではスレッドを利用した場合と比べ、少ない変更点で並列化を実装できていることが分かる(表4)。処理時間はマルチスレッドを利用したものと比べてわずかに多いが、無視できる範囲であろう。

表4 OpenMPによる並列処理プログラムの処理時間
プログラム実行時間
シングルスレッド版(med_serial.c)5553ミリ秒
OpenMP版(med_omp.c)2995ミリ秒