OpenMPメモ

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で書いたほうが自然じゃないかな。

しかし最近は便利になったもんだねぇ。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です