( 付録 A) OpenMP チュートリアル OepnMP は 共有メモリマルチプロセッサ上のマルチスレッドプログラミングのための API です 本稿では OpenMP の簡単な解説とともにプログラム例をつかって説明します 詳しくは OpenMP の規約を決めている OpenMP ARB の http://www.openmp.org/ にある仕様書を参照してください 日本語訳は http://www.hpcc.jp/omni/spec.ja/ にあります また OpenMP のチュートリアル http://www.hpcc.jp/omni/openmp-tutorial.pdf にありますので 参考にしてください 1 OpenMP の特徴と並列プログラミングモデル OpenMP は 新しい言語ではありません C や Fotran などの既存の逐次言語にプラグマ (#pragma で始まるCの指示文のこと ) やコメント行 (Fotran では $! で始まる行 ) で 指示を加えることにより OpenMP の並列プログラミングモデルに従ったプログラミングをするための仕様を定めたものです OpenMP では以下のような特徴があります (1) 既存の逐次プログラムをベースに並列プログラムを作ることができる (2) 指示文を使って スレッドを生成 制御することができ スレッドライブラリなどを使うよりも簡単にスレッドプログラミングができる (3) 徐々に指示文を加えることにより 段階的に並列化をすることができる (4) 基本的に OpenMP の指示文を無視することにより 元の逐次プログラムになります ( 逐次の semantics を保持している ) 従って 逐次と並列プログラムを同じソースで管理することができます このような特徴から MPI のメッセージ通信のプログラミングに比べ非常に簡単に並列化することができます OpenMP の規約では これらの要素を定義しています (1) 指示文 (2) 実行時ルーチン (3) 環境変数図に OpenMP のアーキテクチャの概略について示します OpenMP はこれらの要素を通じて 共有メモリのマルチプロセッサの並列プログラミングモデルを提供しています 共有メモリを使ったマルチスレッドプログラミングでは 共有メモリ上でプロセッサによる複数の実行の流れを制御するプログラムを書きます スレッドとは実行の流れのことで OpenMP では この制御をコンパイラに対する指示文で行います ユーザアプリケーションプログラムユーザ OpenMP のプログラムは通常の逐次プログラムと同じように main から始まります #pragma OpenMP 指示文 構文環境変数で始まる行は指示文といいます C 言語では #pragma omp で始まるプラグマを用います #pragma omp OpenMP 指示文... 実行時ルーチン OpenMP 実行時ライブラリ スレッドライブラリオペレーティングシステム - 1 -
指示文がなければ 通常の逐次プログラムと何ら変わりがありません まず 以下のようなプログラムを考えて見ましょう A... foo(); /*..B... */ fork A C. Call foo() Call foo() Call foo() Call foo() B D join C E... A は通常の逐次プログラムと同じように 実行されます 次に parallel 指 D 示文 に続くブロック文が複数のスレッドで並列に実行されます このブロック文の実行が終わる E と すべてのスレッドの終了を待って 逐次に戻り Cの部分が逐次に実行されます また 次の があると このブロック文が複数のスレッドで実行されます 逐次から複数のスレッドになることを fork 1つのスレッドに戻ることを join といい このような実行モデルは fork-join モデルといいます BやDの parallel 指示文があると この中の文は重複して実行されます 例では Bの関数呼び出しも含めて それぞれのスレッドで実行されます parallel 指示文で複数のスレッドで実行されるブロックを並列リージョンと呼びます また この並列リージョンを実行する複数のスレッドのことを team と呼びます この team 内のスレッドは 0 から番号がつけられており 元の逐次部分を実行しているスレッドは 0 番になり これをマスタースレッドと呼びます 2 Hello World:OpenMP による並列プログラミングさて 具体的な例を使って説明していくことにしましょう まずは 良く C のプログラムはじめに学習する hello world のプログラムを OpenMP 版を考えることします ここではスレッドの番号 ( すなわち 0 から始まるスレッドの番号 ) を出すことにします #include <stdio.h> main() printf("hello world from %d of %d\n", omp_get_thread_num(), omp_get_num_threads()); - 2 -
プログラム中 #pramga で始まる行はコンパイラに対する指示文です # で始まっているので通常の C コンパイラにとってはコメント行です pragma の後の omp キーワードにより OpenMP コンパイラは このコメント行がコンパイラに対する指示文であると認識します parallel 指示文は 次に続く文あるいはブロックを並列に実行するコードを生成させます printf では OpenMP 処理系の実行時ライブラリ関数である omp_get_thread_num 関数および omp_get_thread_threads 関数が呼ばれています これら関数はそれぞれスレッド番号 スレッドの数を返す関数です 上記プログラムを今回使用する OpenMP コンパイラ Omni OpenMP で コンパイルして実行してみましょう コマンドは omcc で /opt/omni/bin にあります % omcc -o omphello omphello.c %./omphello #pragma parallel で指定された部分が それぞれのスレッドで実行され 4CPU のマシンでは 次のような結果が得られるはずです hello world from 0 of 4 hello world from 2 of 4 hello world from 1 of 4 hello world from 3 of 4 OpenMP では並列部分がいくつのスレッドで実行されるのかは プログラムでは指定しません 通常 共有メモリマシンで実行する場合には何個の CPU があるかが実行開始時に調べ その CPU と同じ数のスレッドが生成 それぞれの CPU でスレッドが実行されます 実験用のマシン上では 2CPU なので hello world from 0 of 2 hello world from 1 of 2 となるはずです 確かめてください もしも スレッド数を変えたい場合には環境変数 OMP_NUM_THREADS で制御します csh 環境では 以下のようにして環境変数にスレッド数をセットします % setenv OMP_NUM_THREADS 4 このスレッド数は実際の CPU の数よりも多くても少なくてもかまいません CPU 数よりも少ないスレッド数の場合には一部の CPU しか使われません CPU 数よりも多いスレッド数が指定された場合には オペレーティングシステムのスケジューリングにより各スレッドに C PU が適当にスレッドが割り当てられて 実行されます この場合にはスレッド数を CPU 数よりも増やしたからといって 実行速度が速くなるわけではないことを注意してください ( 実際 遅くなることもあります ) 本当に CPU が並列に動いているのか これを確かめるためには コマンド xcpustate を使います これをバックグラウンドで動かしておきましょう % xcpustate& このコマンドはウインドウ上に CPU の数分だけの棒グラフが出て CPU が動き出すと 赤い棒グラフになって動いているのがわかります hello world のプログラムでは あまりにも実行時間が短くて ちょっとわかりにくいかもしれません このコマンドは 同時に login しているユーザがプログラムを動かしているときにも表示されますから 他の人が使っていないことを確認するにも便利です - 3 -
3 ワークシェアリング指示文の使い方 : ベクトル計算の並列化 OpenMP では 並列リージョンは全てのスレッドで同じコードが実行されます スレッド番号を取得し明示的にマルチスレッドプログラミングをすることもできますが ワークシェアリング指示文を使うことによって ループなどを簡単に並列化することができます ワークシェアリング指示文とは 並列リージョンで team 内のスレッドで指示された文を分割して実行するための指示文です 前に 並列リージョンでは 同じ文を重複して実行すると述べましたが ワークシェアリング指示文のところでは指定された部分を分割して実行します 次の例について考えてみましょう int A[1000]; main() int i; for(i = 0; i < 1000; i++) A[i] = i; printf("sum = %d n",sum(a,1000)); int sum(int *a, int n) int s; s = 0; for(i = 0; i < n; i++) s += a[i]; return s; 関数 sum は n 個の数を加算する関数です これを並列化するためには 加算する配列を分割して 各スレッド (CPU) がその部分を加算し その結果を最終的に合計して 全体の加算をすればいいことになります ( 図 ) このようなプログラミングの場合には for 指示文が便利です for 指示文は ループを並列化するためのワークシェアリング指示文です int sum(int *a, int n) int s; s = 0; #pragma omp for reduction(+:s) for(i = 0; i < n; i++) s += a[i]; return s; 1 2 3 4 1000 逐次処理の場合 + S 1 2 250 251 500 501 750 751 1000 + + + + プロセッサ 1 プロセッサ 2 プロセッサ 3 プロセッサ 4 + 並列処理の場合 S - 4 -
parallel 指示文で生成されたスレッドは for 指示文により for ループの各部分を分担して実行します for 指示文は 並列リージョンを実行する複数のスレッドで for 指示文の後にあるループを並列に実行します 例えば 4 スレッドで並列実行している場合には 上の例では i が 0 から 249 まではスレッド 1, 250 から 499 まではスレッド 2... というように各スレッドで並列に実行します この場合は均等にあらかじめ分割して実行しますが ループの実行時間がばらつく場合などには動的にループを実行するなど 実行の仕方も指定することができます for 指示文では 並列実行するループを全てのスレッドがそのループの実行を終了するまで 待ち合わせます 並列リージョンに 1 つの for 指示文で指定される並列ループのみがある場合には 以下のように 1 つにすることができます for reduction(+:s) for(i = 0; i < n; i++) s += a[i]; さて OpenMP の指示文にある reduction は何をしめすのでしょうか? この文は 変数 s が共有されて加算される変数であることを指示します このような指示句をデータスコープ属性の指定するものです 例えば 次のような例を考えてみましょう #pragma omp for private(t) for(i = 0; i < 1000; i++) t =...;...=... t...... for 指示文の後にある private(t) は ループ並列実行する場合に変数 t をそれぞれのスレッドで別々の変数を持つことを指定するもので 変数データのスコープ属性の指定をするものです 通常 なにも指定しない変数は全てのスレッドで共有されます しかし 例にある変数 t のようにループ内で一時的に使われる変数の場合は private(t) がないと並列実行しているスレッドが同じ変数に書きこんでしまうため 正しく並列化ができなくなります データスコープ属性には以下の種類があります shared(var_list) 構文内で指定された変数がスレッド間で共有される private(var_list) 構文内で指定された変数が private firstprivate(var_list) private と同様であるが 直前の値で初期化される lastprivate(var_list) private と同様であるが 構文が終了時に逐次実行された場合の最後の値を反映する reduction(op:var_list) reduction アクセスをすることを指定 スカラ変数のみ 実行中は private 構文終了後に反映 4 その他の指示文 ワークシェアリング指示文には ループを並列化する for 指示文の他に 1 つのスレッドのみで実行する single 指示文 異なる部分を別々のスレッドで実行する section 指示文があります 以下のコードでは single 指示文で指定されたブロック文は一つのスレッドでしか実行されません - 5 -
#pragma omp single statements この指示文では すべてのスレッドが到着するまで 待ち合わせをします section 指示文では #pragma omp sections で囲まれたブロックのなかで #pragma omp section で指示された部分は別々のスレッドで実行されます これを使っていわゆるタスク並列のプログラミングを行うことができます #pragma omp sections #pragma omp section section1 #pragma omp section section2 また この指示文でもすべてのスレッドはこの指示文を実行するまで待ち合わせます で複数スレッドで実行させる時に すべてのスレッドを待ち合わせる操作がバリア操作です この操作を行う指示文がバリア指示文です #paramga omp barrier なお OpenMP の指示文は複数のスレッドで実行されている場合にしか有効でありません つまり で指定される並列リージョン以外では無効になります ( だだし 並列リージョン内から呼び出された関数でも 複数のスレッドで同時に実行されていますから 有効になることがあります ) 5 Laplace 方程式 OpenMP による Laplace 方程式プログラムを図に示します 元の逐次版のプログラムに 5 行のコンパイラ指示文を加えるだけで並列化できます #pramga omp parallel で do ループ全体を並列化しています 各 for 指示文は ループの並列化を行っています parallel 指示文で指定された並列リージョンでは 複数のスレッドで実行されます ワークシェアリング指示文では それらのスレッドでループを分割して並列実行することに注意してください スレッドは並列リージョンの最初で生成され ループごとに生成されるわけではありません #paramga omp signle では 1 つのスレッドだけで 変数 err を初期化します その後 の for 構文では err に対して reduction が指定されています このプログラムは 通常のコンパイラで指示文を無視してコンパイルすることで 元の逐次プログラムとして実行することができます main() double start, end; double err, diff; int i, j; init(u); init(uu); start = second(); - 6 -
private(i, j, diff) do /* copy */ #pragma omp for for (i = 1; i < YSIZE - 1; i++) for (j = 1; j < XSIZE - 1; j++) uu[i][j] = u[i][j]; /* update */ #pragma omp for for (i = 1; i < YSIZE - 1; i++) for (j = 1; j < XSIZE - 1; j++) u[i][j] = (uu[i - 1][j] + uu[i + 1][j] + uu[i][j - 1] + uu[i][j + 1])/4.0; #pragma omp single err = 0.0 #pragma omp for reduction(+:err) for (i = 1; i < YSIZE - 1; i++) for (j = 1; j < XSIZE - 1; j++) diff = uu[i][j] - u[i][j]; err += diff*diff; while (err > EPS); end = second(); printf("time = %f seconds\n", end - start); サンプルプログラムとして 準備しておきますので 2CPU の実験用のマシンで約 2 倍になることを確認してみましょう 最初に述べたように このプログラムを普通の C コンパイラでもコンパイルできます この場合は単なる逐次プログラムになります % cc -o seq laplace.c でコンパイルした逐次の seq と % omcc -o omp laplace.c としてコンパイルした並列版の omp とを実行して実行時間を比較してみてください - 7 -