並列化 プログラミング
能書き
OpenMPの命令、「OpenMP指示子」をまとめます。OpenMPのコマンド:基本 + ループ並列化
OpenMPを使うためのライブラリ(組み込みモジュール)を呼ぶ!$ use omp_lib
並列実行を開始する(環境変数OMP_NUM_THREADSに指定された数だけスレッドを立てる)
!$OMP parallel [options]並列実行を終える(メイン以外のスレッドを終了させ、単一スレッド処理に戻る)
!$OMP end parallelこの2つの命令の間を、並列実行領域と呼ぶことにする。
ループを自動分割し、各スレッドに割り当てる
!$OMP do [options] do i=1,n {ループごとに独立な処理} end do !$OMP end do当然ながら、並列実行領域でのみ使用可能。
並列化とループ分割をセットにした省略形も用意されている。
!$OMP parallel do [options] do i=1,n {ループごとに独立な処理} end do !$OMP end parallel do
具体例1:単純なループ並列化
program paralleldo !$ use omp_lib implicit none integer :: i integer :: m(10) !$OMP parallel ! 並列処理開始 !$OMP do do i=1,10 m(i) = i end do !$OMP end do !$OMP end parallel print *, "m = ", m(:) end program一番よく使うのがこのパターンです。 『!$OMP do』がなければ全員で同じことを実行するだけですが、 『!$OMP do』の直後のループは、それぞれのスレッドに振り分けられて実行されます。
イメージとしては「i=1,2,3はCPU1が担当、i=4,5,6はCPU2が担当、・・・」という感じです。
深くは立ち入りませんが、ループの割り振り方はいろいろあります。
ループ並列化の応用:総和並列化
和を取る作業を並列化するにはどうすれば良いでしょうか。一見、共有変数に足し合わせておけば良いように思いますが、それは危険です。
なぜなら、それぞれのCPUの読み出しと書き込みのタイミングによって異なる(誤った)結果が出る可能性があるからです。
時々正しい結果になる事もあるので、知らないとデバッグに時間がかかります。
こういう時は、
!$OMP do reduction(演算子:変数)というオプションを指定します。
sum = 0 !$OMP do reduction(+:sum) do i=1,n sum = sum + i end do !$OMP end doこれで、各スレッドで和を取った後に安全に足し合わせてくれます(初期値も引き継ぐ)。
内積を取る時にも使えます。
OpenMPのコマンド:section
1つの"section"を1つのスレッドに割り当てる!$OMP sections !$OMP section {処理1} !$OMP section {処理2(処理1とは独立)} !$OMP end sectionsループではない任意の処理を分割する命令です。
非並列実行
!$OMP single {並列化したくない処理} !$OMP end single
非並列化
並列化できる領域が2箇所あった場合、それぞれに$OMP parallelと$OMP end parallelを書くのが基本です。 ただし、その間の領域が短い場合、オーバーヘッドを避けるために「並列化したまま1スレッドだけ実行させる」という技があります。!$OMP single通常の書き方
!$OMP parallel do {並列化したい処理1} !$OMP end parallel do x = -x !$OMP parallel do {並列化したい処理2} !$OMP end parallel do少し速くなる(かもしれない)書き方
!$OMP parallel !$OMP do {並列化したい処理1} !$OMP end do !$OMP single x = -x !$OMP end single !$OMP do {並列化したい処理2} !$OMP end do !$OMP end parallel
singleを使う必要が無い場合(結果が変わらない)
!$OMP parallel !$OMP do {並列化したい処理1} !$OMP end do n = 2 !$OMP do {並列化したい処理2} !$OMP end do !$OMP end parallel
並列化 変数の扱い
OpenMPによる変数の扱い
並列化する場合、変数をスレッド間で共有する(shared)かしない(private)かを意識する必要があります。OpenMPのdefaultはsharedです。例外として、並列化したループ変数のみ自動的にprivateになります。
モジュールのグローバル変数も共有されてしまうので注意が必要です。
各スレッドが呼び出すサブルーチン内で宣言された変数(自動変数)はprivateです。
変数の共有指定
プライベート変数を指定する(初期値は未定義になる)!$OMP parallel [do] private(変数1,変数2,配列1,...)
巨大な配列をprivateにすると、stack over flowを起こす可能性があります。
初期化されたプライベート変数を指定する(初期値を引き継ぐ)
!$OMP parallel [do] firstprivate(変数1,変数2,...)(使う場面はあまりありません)
共有変数を指定する
!$OMP parallel [do] shared(変数1,変数2,...)
モジュール変数のプライベート指定
module paramod integer :: 変数1,変数2,配列1... !$OMP threadprivate(変数1,変数2,配列1,...) contains ... end module
前述のprivateはスタック領域、threadprivateはヒープ領域を使うので、こちらの方が安全です。
privateの例
2重ループの内側の変数などの一時変数はprivate指定する必要があります。!$OMP parallel do private(i,j,temp) do i=1,n do j=1,m temp = i+j {処理} end do end do !$OMP end parallel do1つめのループ変数は指定してもしなくても構いません。
複雑な場合
長いループを並列化する場合、全ての変数を指定した方が安全です。!$OMP parallel do default(none) & !$OMP & private(i,j,temp) shared(n,m,vec) do i=1,n temp = 0 do j=1,m temp = temp + i*j end do vec(i) = temp end do !$OMP end parallel doどの変数をどう扱うべきか、慎重に考えましょう。
ちなみに長い行を分割するときは!$OMPを忘れずに。
並列化 高速化のためのオプション
最適化オプション
結果が変わらないオプション。最適化用。nowait
通常は、並列化ブロックごとに同期処理(すべてのスレッドが終わるまで待つ)が取られます。待つ必要が無い場合は、その時間を短縮できます。
!$OMP end do nowait
!$OMP end sections nowait
!$OMP parallel private(i,j,temp) !$OMP do do i=1,n {処理1} end do !$OMP end do nowait !ここで止まらず、終わったスレッドは次に進む !$OMP do do i=1,n {処理1に依存しない処理2} end do !$OMP end do !$OMP end parallel
ループの分割方法
ループの分割の仕方によっては、大きな待ち時間が発生することがあります。上手く分割すると、その時間を短縮できます。
!$OMP do schedule(タイプ[,チャンクサイズ])static(デフォルト) チャンクサイズごとに分割し、番号順に静的割り当て
チャンクサイズはデフォルトでループ回数/スレッド数。負荷が均等なら最も効率が良い。
dynamic チャンクサイズごとに分割し、終わった順に動的割り当て
ループごとの負荷に偏りがある場合に良い。
guided チャンクサイズを小さくしながら割り当て
上2つの中間的な感じ。
チャンクサイズは、大きいほどオーバーヘッドが減り、小さいほど負荷の偏りが小さくなる。
並列化 実行時オプション
実行時オプション
OpenMP並列化による並列実行スレッド数を指定する。OMP_NUM_THREADS=並列数デフォルトは環境依存(1つしか使わないor全部使い切る)なので、常に付けるべき。
MKLの並列実行スレッド数を指定する。
MKL_NUM_THREADS=並列数LAPACKを使う場合、OMP_NUM_THREADSと同じ値を指定する。
例:
$ env OMP_NUM_THREADS=4 MKL_NUM_THREADS=4 ./a.out
OMP_STACKSIZE=1スレッドあたりのスタックサイズデフォルトは4M。OpenMPのprivate変数は全てstackに積まれるので、大きな一時配列を使う場合は指定する必要あり。
$ env OMP_NUM_THREADS=4 MKL_NUM_THREADS=4 OMP_STACKSIZE=16M ./a.out