OpenMP は共有メモリ並列プログラミングの標準 API である。ここでは C/C++ 版を紹介する。Fortran 版もある。
OpenMP は 2008 年 5 月現在バージョン 3.0 が公開されているが,あまり新しい機能は多くのコンパイラで実装されていないと思われる.以下の説明は,かなり古いバージョンに従っているので,たいてい動いてくれると思う.
OpenMP は自動並列化ではない。依存性の分析と解決はプログラマに任されている。使い方を間違えば、当然正しくない結果を出す。
以下の説明は OpenMP を使うにあたって最低限の中身に絞ってある.説明も相当簡略なので,この資料だけで OpenMP のプログラミングができるとは思わないほうがよい(もうしわけないが).OpenMP の仕様書は最近はずいぶん分厚くなったが,仕様の理解を助けるサンプルプログラムがたくさんあるおかげである.あの一連のサンプルプログラムは,OpenMP の正しい使い方を理解するには大変便利である.ぜひ一読されたい.
OpenMP を使うときにはインクルードする。
OpenMP コンパイラが define するマクロ。OpenMP が動かないコンパイラでおかしくなる部分を調整する.
Fortran では !$ から始まる行は OpenMP のみコンパイルする
「節」には何種類かの「節」が入れられる.(以下同様)
次の行から始まるブロック(文か複文)を並列に(重複して)実行する。
スレッドの数は num_threads 節として
次のブロックをループ並列化する。並列化の仕方として4通りが指定できる。
メモリの一貫性を取る。スレッド間でのメモリアクセスの一貫性は通常は確保されていない。flush が行われるところだけでメモリの一貫性が取られる。
但し、barrier, criticalとparallel の出口と入口、for と singleの出口では暗黙のflushが行われる(但しnowait 節を入れるとflushされない)。
逆に、forとsingleの入口およびmasterの入口と出口では暗黙のflushは行われない。
スレッドの数を返す(Fortran では関数)
スレッド番号を返す(Fortran では関数)
ほかにも section とか lock とかいろいろあるが,省略する.MPI でもそうなのだが,多くのプログラミングに必要なのは機能のごく一部だけである.極端な話,
OpenMP バージョン 3.0 から task というのが追加されたらしい.なかなか便利そうなので,実装されたコンパイラがでてきたら使ってみたいものだ.
OpenMP は比較的シンプルな並列化を想定しているようである。 Fuzzy barrier や一対一の同期は提供されていない。また、配列に対するリダクションも C 言語版には 含まれていないようである(fortran では制限があるが使える)。
OpenMP のプログラムは、指示行を無視してやれば逐次処理のコンパイラで動くのが特徴である。 これは結構デバッグの役に立つので、関数などを使って明示的な処理をする場合にも _OPENMP マクロ などをうまく使って、逐次実行ができるように工夫するべきである。
parallel 指示行においてスレッドの生成が行われるので、できるだけ parallel 指示行は少ない方がよい。 ほぼプログラム全体を覆うような parallel 指示行を一度だけ書くのがよいとされる。しかし、実装が 適切であればスレッド生成のオーバーヘッドは軽いようである。
parallel 指示行とその他の指示行とが、異なる関数に含まれていても良い。parallel 指示行の静的な スコープに入っていない指示行を orphan directive と呼び、これらは parallel 指示の範囲外で あれば無視される。
一般的に変数のそれぞれが私有なのか共有なのかには十分な注意が必要である。私有のつもりで 共有変数を使うと、とんでもない結果が簡単に出てしまう。
parallel 指示のされているブロックの前後では、私有変数は別物である。その前後で変数を一貫して 使いたい場合には、firstprivate, lastprivate, copyin などの指示が必要となる。
parallel ブロックを一つの関数にしてしまうと間違いがすくないようである。
私有変数のデータを他のスレッドに送るには、「通信」のための共有変数を準備しておく必要がある。 これは結構な手間がかかる。
OpenMP で一番標準的な使い方と考えられているのがこの for 指示のようである。しかし、実際には for のような simple mind な並列化手法で高い性能を得るのは難しい。
共有メモリといってもキャッシュは CPU 毎に付いているので、キャッシュデータの再利用を考慮してタスク割付を行わなければ性能が下がる。 結果的には omp_get_thread_num などを使って明示的な SPMD 形式のプログラムを組むのが確実である。
共有メモリを介してデータをやり取りする場合には、生産者のデータが確実にメモリに書き込まれた ことを確認するまで消費者は読出しを待つ必要がある。この待ち合わせが同期である。
データ並列のような並列処理の場合には、バリア同期で事が足りることが多い。すなわち、
タスクプールのような動的なスケジューリングを行う場合には、critical を用いる。さらに 手の込んだことがしたい場合には、lock を用いることになる。lock には暗黙の flush が付いていないことに注意。
共有メモリを介してデータをやり取りする場合には、flush は不可欠である。共有変数に volatile を指定すればよさそうに思われるが、実際にはそれでは不充分である。 その最大の理由は、
そこで提案された解決策が weak consistency model と呼ばれるものである。すなわち、 特別な同期命令 (i.e. flush) の前後で実行の順序とメモリアクセスの完了を保証するのである。
単純化すると、
さらに困ったことに、OpenMP で書いたプログラムよりも、MPI で書いたメッセージ通信のプログラムの 方が性能が高いという報告がかなりある。こうなってしまうと、一体 OpenMP の存在意義はどこに あるのか分からなくなってしまう。
あえて共有メモリの利点を探せば、次のようなものが考えられる。
デバッグに関して言うと、共有メモリの方が難しいように思われる。分散メモリの場合には, 明示的に receive をしなければ他人のデータは自分の計算の邪魔をしない.しかし共有メモリでは 相手が予期できない勝手なタイミングで共有メモリ部分にデータを書き込んでくる.タイミングは毎回 異なるので,エラーが生じる場所も毎回異なる.だれがどこで書き込んだどのデータが悪いのかを特定 するのは非常に難しい.
最大の救いは、OpenMP のプログラムはうまく書いておけば、そのまま逐次モデルでのコンパイルと 実行ができるということである。また、明示的なデータ分散は必ずしも必要ないので、漸進的な 並列化が容易であるということである。しかし、以下に示すように、高性能化はかなり難しい。
データローカリティ:キャッシュマシンであれば、データのローカリティが性能に大きな影響 を及ぼす。残念ながら、共有メモリのプログラミングは局所性が悪くなる傾向にあるようである。CPU 毎の データ局所性を改善するには、データの配置を工夫したり、一部を私有化したりする必要がある。これは 実質的に「データ分散」を要求することになる。
メモリアクセス衝突:バス結合の SMP では、複数の CPU が同時にメモリをアクセスすると バスの取り合いが生じる。このためメモリアクセスが性能を決めているようなプログラムでは CPU 数を 増やした分だけバス性能が実効的に下がり、CPU あたりの性能が下がる。スイッチ結合の SMP の場合には スループットは確保されるが、同じメモリブロックにアクセスがあれば衝突により遅延が増大する。この問題を 解決するには、キャッシュデータの有効利用と、遅延隠蔽のための工夫が必要である。
「通信」遅延:スレッド間でデータをやり取りする場合には「通信」が必要となるのは当然である。 これは共有メモリを介して行われるが、キャッシュよりも遅いメモリを通すために、それなりの時間がかかる。 共有メモリハードウェアであっても、できるだけ「通信」量は減らすような工夫をしたほうが良い。
False sharing:キャッシュの一貫性は、通常ライン単位で行われている。実際にデータをやり取り するつもりでなくても、偶然同じラインに乗ってしまったデータを異なる CPU がアクセスすれば、キャッシュの 一貫性操作が行われ、性能が下がることになる。これが false sharing である。私有データのメモリ上の配置に 工夫をする必要がある。
同期などのオーバーヘッド:同期や flush、スレッドの生成などは、並列化しなければ必要ない部分 であるから、純粋な並列化オーバーヘッドである。できるだけこれらは減らしたいところであるが、無理をすると バグを発生するもとになる。安全のために多めに同期をいれてしまうことが多い。
負荷の不均衡:負荷の不均衡は、メモリモデルによらずに並列性能に影響を及ぼす。共有メモリ の場合、false sharing やメモリアクセス衝突のような、定量的に予測しにくい要因でスレッド毎の実行時間に 違いが出てくる。このため、計算量だけ考えても完全な負荷の均衡化が実現できないという問題が発生する。
読み出し型の情報交換:分散メモリでは,データを生成したプロセッサが,それを必要としている プロセッサに送ってやる書き込み型の情報交換となっている.従って,データの転送は1回で済む.共有メモリでは, データを使用するプロセッサがそれを持っているプロセッサにデータを要求する.従って,データの要求とデータの 転送の2回の通信が必要となる.また,キャッシュライン単位の転送となるため,必要以上のデータが送られること も多い.
性能評価の難しさ:分散メモリでは,性能の劣化の原因となるのは負荷の不均衡とメッセージ通信の オーバヘッドである.これらのものは(かなり複雑な場合もあるが)それなりの精度で予測できる.それ以外に 通信遅延の隠蔽やキャッシュヒットの向上などで思わぬ性能向上が得られる場合が多い.共有メモリでは, 性能劣化の原因は負荷の不均衡と同期のオーバーヘッドのほか,キャッシュミスヒットの増大や false sharing が ある.同期のオーバーヘッドは小さいが,キャッシュミスが性能におよぼす影響は評価が難しく,予想外に性能が 伸び悩むことになる.
旧バージョン(6年前)ではここに SMP クラスタに対する疑問をいろいろ書いておいた.当時は SMP マシンはとても高価で,2 CPU のマシンを 4 台ならべるのに比べて,1 CPU のマシンを 8 台並べた方が,ずっと安価で性能も出た.
ところが,もはやマルチコアでない CPU はほとんど売っていないという状況である.クラスタを組めば SMP クラスタになるのは必然である.当時のコメントはもはや通用しない.そもそも SMP (Symmetric Multi Processor) という言葉自体が死語になっている(注:複数の CPU を載せた,共有メモリ型の PC のようなものだった.今でもマルチコアは安いがマルチ CPU は高いよね).注意をひとつだけ残しておこう.
それは,共有メモリマシン上でも MPI で並列化した方が性能が出る場合があるということである.したがって,マルチコア機をつなげたクラスタでも,総コア数だけのプロセスを立ち上げた MPI (フラット MPI)のほうが高い性能になることがある.
しかし,通信量がかなり多くなると,マルチコアの並列性は OpenMP で,ネットワーク越しの並列性は MPI で書くハイブリッド並列化のほうが高い性能がでると言われている.ネットワーク間の通信を複数のコア上のプロセスがばらばらに実行するよりも,交通整理をしたほうがよいというわけである.しかしプログラミングはなかなか大変だ.