gccでのコンパイルはオプションに-fopenmpを追加。gcc 4.1系以降はデフォルトで入ってる。
# gcc –fopenmp program.c –o program
_OPENMPがdefineされる。
#ifdef _OPENMP printf("OpenMP enabled\n"); #endif
スレッド数はデフォルトでコア数なので特に設定する必要はない。上書きしたいときは環境変数「OMP_NUM_THREADS」で設定。
# OMP_NUM_THREADS=10 ./program
スレッド数をプログラム側で指定する方法もあるが、まあ使うことはないでしょう。
コードの書き方。単純なfor文には直前に1行追加。データ分割で、配列アクセスならコア毎のキャッシュに当たりやすく、性能が出やすい。OpenMPの最も得意な形。
#pragma omp parallel for for(int i=0; i<N; i++){ ... }
これで効果が出やすいのは、、、
- それぞれのイテレーションで計算量が同じ(各スレッドに同量程度の仕事が割り振られる)
- ループを回る回数が多い(スレッド生成・joinにもある程度コストがかかる)
- 配列アクセス(データがキャッシュに当たりやすい)
というもの。
外から入ってくる変数でスレッドローカルにしたいものはprivate(var1, var2)などで宣言。デフォルトではスレッド間で共有される。中のスコープで定義した変数は当然、スレッドローカル。中のスコープでもstatic変数は共有(スレッドですから)。
for文のイテレータ変数も、for文内部で宣言しているのでなければ、privateにしておいたほうが良いのかな? 書かなくても良いかもしれないが、書かないと気持ち悪いね。
int i; #pragma omp parallel for private(i, x) for(i=0; i<N; i++){ x=... }
基本的には、forブロックの内部で外の変数に代入している部分があったら、そいつは怪しい。テンポラリで使っているだけならprivateに入れるか中のスコープで宣言する。
int i, x; #pragma omp parallel for private(i) for(i=0; i<N; i++){ int y; x=... // xは共有:怪しい y=... // yはローカル:怪しくない }
少し複雑なfor文なら、2行追加。次期版(OpenMP 3.0?)でタスク並列がサポートされるらしく(?)、それまでのつなぎの書き方かな。トリックですね。
#pragma omp parallel for(x_iterator i=x.begin(); i!=x.end(); i++) #pragma omp single nowait { ... }
parallelブロックでスレッドが生成されてスレッド数ぶん実行されますが、singleブロックは1スレッドでしか実行しないという意味になります。他のスレッドが実行していたら飛ばして次に進むので、空いたスレッドから順番にタスクを実行していく感じになる。singleだけだと毎回バリア同期が入るけど、nowaitを入れると待ち合わせをしなくなり、望み通りの動作に。
ただし、このsingle nowaitブロックの中ではcontinueやbreakが使えず、コンパイラがエラー(invalid exit from OpenMP structured block)を吐きます。#pragma omp parallel forブロックならcontinueは書けます(breakは書けない:break statement used with OpenMP for loop)。そこが違う。
#pragma omp parallel for(x_iterator i=x.begin(); i!=x.end(); i++) #pragma omp single nowait { if(...) continue; // error! }
ネストすると内側は並列化されない。環境変数「OMP_NESTED」をTRUEにすれば内側もスレッドができるが、この機能はあまり使うことはないんじゃないかと思う。効果がないと言うより、悪影響がないと言った方が正確か。ネストはあまり気にせず、重そうなループは#pragma書いておけばいいんじゃないかと思う。
アトミックにしたい部分は#pragma omp atomic。lockプレフィックス扱い。ゆえに、++や–など、短いオペレーションのみ対応。
クリティカルセクションは#pragma omp critical (名前)。「(名前)」を省略したらジャイアントロックのようになる。mutexロックのような扱い。ブロックを書ける。名前はロックの名前で別のコードブロック間で同じロックを扱えるようになっている。
バリア同期は#pragma omp barriar。全部のスレッドがこの部分に来るのを待つ。#pragma omp parallelの外で待ってくれるので、あまり使わないと思う。
reductionは使いづらい。単純な合計程度なら良いが、使える演算がかなり限られているようだ。reduceの関数を呼べるようになれば使いではありそうだが、現状だと覚える必要はなさそう。最後にatomicかcriticalで書いたほうが自然じゃないかな。
しかし最近は便利になったもんだねぇ。