2016. 2. 16.
2016. 2.20. Debug版でのビルド結果であることを明示し、Release版では異なる結果になることも記す。
2016. 2.29. OpenMPで、二重ループを避けるべき、との部分を誤解しないように、丁寧に説明した。
        画像処理のような二次元データは、一次元化した方が良い、との意味である。
石立 喬

OpenMPとVisual C++による並列処理(1)

――― OpenMPを使えるようにして、forループの並列演算を試みる -----

 マルチコアCPUが一般的になって、マルチスレッドでプログラムを並列化することが容易になった。並列処理のライブラリとして知られるOpenMP APIが、Visual Studio Community 2015から使用できるので、並列処理の第一歩として、forループの並列化を試みた。
 実行時間の測定方法は、「Visual Studio Express 2013の新しい使い方(5)」に示したものを使用した。ビルドは、Debug構成で行ったが、並列処理の真の実力を知るにはRelease構成にする必要がある。

OpenMP(Open Multi-Processing)とは
 オープンソースの並列処理APIで、メモリ共有方式(一台のコンピュータ)に特化していて、OpenMPライブラリの他、コンパイラに並列化の具体的な方法を指示する指示文、指示節、、使用するスレッド数などの実行環境を指示する環境変数から成っている。
 コンパイラに対しては、#pragma omp に続く指示文(directive、ディレクティブ)、指示節(clause、クローズ)で並列化の指示を与える。指示文、指示節は多数あるので、詳細については、使用の都度説明する。
 OpenMP APIは、1997年にFortran用に発表され、1998年にC/C++用も用意されたもので、以後、バージョンアップが重ねられて、最新版は、Fortran C/C++共用のバージョン4.5(2015.11.15)であり、OpenMP ARB(Architecture Review Board)の非営利組織が管理している。
 Visual Studioに関しては、過去には、Professional版のみしか使用できなかったが、現在は、無償のVisual Studio Community 2015にもOpenMP Ver. 2.0が組み込まれていて、特に、別途ダウロードする必要はない。ただし、OpenMPが最新のバージョンでないことは気になる。
 最近のマルチコアCPUの普及に伴い、OpenMPを使用するアプリケーションが増えており、特に、画像処理は、並列処理が可能な部分が多いので、効果的な分野である。

Visual Studio Community 2015からOpenMPを使用可能にする
◎空のWin32コンソールアプリケーションを作成する
1)Visual Studioを立ち上げ、「新しいプロジェクト」をクリックする。
2)「新しいプロジェクト」ウインドウで、
     左欄 --- Visual C++
     右欄 --- Win32コンソールアプリケーション
     名前 --- OpenMPApplication1(一例)
 として「OK」をクリックする。
◎「プロパティページ」ウィンドウでOpenMPのサポートを指定する
1)メニューから、「プロジェクト」→「OpenMPApplication1(一例)プロジェクトのプロパティ」を選択する。
2)開いた「プロパティページ」ウィンドウで、
     左欄 --- 「構成プロパティ」→「C/C++」→「言語」
     右欄 --- 「OpenMPのサポート」を選択し、右端の「V」をクリックして「はい(/openmp)」を選択する
 として「OK」をクリックする。図1は、「OK」をクリックする直前の「プロパティページ」である。


図1 プロパティページで、/openmp を設定する


使用した指示文(directive、ディレクティブ)と指示節(clause、クローズ)
 コンパイラに対しては、#pragma omp に続く指示文(複数を並べても良い)、指示節(複数可)で指示を与える。以下に、今回使用したもの、または説明上で関連するものについて簡単に示す。
◎指示文
  parallel ------ 並列演算させるべき領域を指定する。範囲は{ }で指定する。括弧を使用しないときは、直後の一行のみ。
  for --------- parallel領域内のforループをスレッドに分ける。
  master ------ parallel領域内のmasterスレッド(0番スレッド)のみを実行する。
  single ------- masterに似ているが、スレッド番号を指定せず、任意のスレッドで実行する。
  critical ------ 他のスレッドが実行中は排他的に避けて、一個のスレッドのみが実行する。
  barrier ------ 全てのスレッドでの実行が終わるまで待つ。
◎指示節
  shared ------ 共有変数であることを明示的に指定するのに用いる。デフォルトである。
  private ------ 共有でなく、スレッドごとに独立している個別変数を指定する。多重forループの内側の変数に必要。
  reduction ---- スレッドごとのprivate変数を,、最終的に一つのshared変数にまとめる(reductionする)ことを指示する。

簡単な一重forループで、並列化の効果を調べる
 まずは、一重のforループを使って、並列化の効果を調べた。forループ内の処理は、特別に意味はなく、時間を費やすために、三角関数を二回呼び出し、乗算してsumに足し込んでいく。
 このプログラムは、下記から成る。
 1)時間測定のための<chrono>、OpenMPの<omp.h>、文字出力のための<iostream>をインクルードする。
 2)文字出力のためのstd、時間測定のためのstd::chronoの名前空間を指定する。
 3)参考のために、使用できるスレッド数を求めて、表示する。
     #pragma omp parallelで並列化してから、#pragma omp singleで、一個のスレッドのみでcoutを実行する。
 ここまで、図2を参照。
 4)OpenMPを用いない、通常のforループを実行し、実行時間を求める。
   時間測定の方法は、「Visual Studio Express 2013の新しい使い方(5)」を参照されたい。
 5)OpenMPを用いて、前述と同じforループを実行し、実行時間を求める。
     #pragma omp parallelの後に、forを追加し、reduction(+:sum)を指示節として記述する。
 ここまで、図3を参照。


図2 プログラムの最初の部分で、スレッド数を確認する



図3 非並列化と並列化の実行時間を比較する


簡単な一重forループの並列化で得られた結果
 実行時間は、実行の都度変動し、OpenMPによる並列化の効果も、バラツキがあった。表1は、8回繰り返した結果を示す。

表1 一重forループにおける、非並列と並列での実行時間の比較


 4スレッドであっても、実行時間は1/4にはならず、最良は27%、平均は36%であった。オーバーヘッドを考えると、これは満足できる数字である。一番良かった結果を図4に示す。


図4 計算結果は同じで、並列化した実行時間は、約27%になっている


スレッド数の確認に使用したcoutがthread-safeでないことについて
 スレッド数は、omp_get_num_threads()関数で求まる。もちろん、あらかじめ、#pragma omp parallelで並列化宣言して後のことである。スレッド数を出力表示するには cout を用いるが、これがthread-safeでないところが問題である。thread-safeとは、複数のスレッドが実行中に、互いに競合状態にならないことを意味する。cout には、これが保証されていない。
 thread-safeでないことを確認するために、cout を単純に並列化すると、スレッドごとに、合計4回実行する。すなわち、
   #pragma omp parallel
   {
      cout << "スレッドの数は " << omp_num_threads() << " です。" <<endl;
   }
を実行してみると、図5のように、正しく表示される場合もあれば、図6のように、一つのスレッドを実行中に、別のスレッドが割り込んでくる場合もあることが分かった。これを改善するには、#pragma omp critical を使うと良く、実際に確認できた。


図5 正しく実行されている



図6 一つのスレッドの実行中に、別のスレッドが割り込んでいる


 ここで紹介するプログラムの目的としては、スレッド数を一回だけ表示すればよいので、図2に示したように、#pragma omp singleを使用した。#pragma omp masterでも同じ結果になる。

二重forループで、並列化の効果を調べる
 一重forループのプログラムを二重ループ化したものが図7である。内側のforループの変数jは、private(j)指定節を用いて、スレッドごとに独立させる必要がある。ここで、注意すべき点は、一重ループでできるものを、「二重化」した場合であって、一般的な二重ループとは異なる。


図7 二重forループの場合の比較


二重forループの並列化で得られた結果
 実行時間は、やはり、実行の都度変わり、並列化の効果も、さまざまであった。表2は、8回繰り返した結果を示す。

表2 二重forループにおける、非並列と並列での実行時間の比較


 4スレッドであっても、実行時間は最良で44%、平均で63%程度にしか短縮されない。非並列(Visual C++のデフォルト)の演算時間は、二重forループの方が短いが、OpenMPを用いた並列化の結果は、かえって長くなっている。OpenMPを用いた並列化は、一重ループで処理できるものは、なるべく一重で行うべきことが分かった。
 並列化による短縮率が一番良かった結果を図8に示す。


図8 並列化による改善が少ない


結 論
 ここで調べたケースに限って結論すると、OpenMPによるforループの高速化は、単純な一重forループでは、Visual C++のデフォルトのままの並列化しない場合に比べて、最も良いケースで27%に実行時間が短縮された。オーバーヘッドも考えると、4個のスレッドでの成果としては十分であろう。
 本来、一重ループでできるものを、無理やり二重forループ化した場合では、やや期待外れであった。最も良いケースで、44%にしかならなかった。
 OpenMPでの高速化には、一重化できるものは、なるべく一重のforループを用いるべきとの結果になった。すなわち、二次元データから成る画像処理においては、X方向、Y方向で処理するよりも、ピクセルを一次元的に並べて処理をした方が早いという結果になった。
 以上は、Debug版であるため、Release版では、異なる結果となる可能性がある。

参考資料
"OpenMP C and C++ Application Program Interface Version 2.0 March 2002"
http://www.openmp.org/mp-documents/cspec20.pdf

"OpenMP Application Program Interface Ver. 4.0. July 2013"
http://www.openmp.org/mp-documents/OpenMP4.0.0.pdf

"OpenMP Application Program Interface Examples Version 4.0.0 November 2013"
http://openmp.org/mp-documents/OpenMP4.0.0.Examples.pdf





「Visual C++の勉強部屋」(目次)