並列化 プログラミング

能書き

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 do
1つめのループ変数は指定してもしなくても構いません。

複雑な場合

長いループを並列化する場合、全ての変数を指定した方が安全です。
 !$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