2016. 2. 24.
2016. 3. 2.  Release構成の単純代入処理ループで、「OpenMPを使うと、かえって悪くなる」の表現は、
        「ほとんど改善効果はなく、確率的には、かえって悪くなる場合もある」と、弱めた。
石立 喬

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

――― Visual C++のデフォルト並列化機能と、Qpar、OpenMPの付加効果を比較する -----

 Visual C++では、自身がデフォルトで内蔵している自動ベクトル化コンパイラオプションQvecの他に、すでに紹介したOpenMP APIの付加と、ここで新たに紹介する自動並列化オプションQparの付加ができる。
 そこで、ここでは、Visual C++のデフォルト、Qparの付加、OpenMPの付加の三種類を用いて実行時間を求め、どのような場合に、どれを使用すると良いか、を調べた。

並列処理のいろいろ
 並列化手法を分けると、ベクトル化(vectorization)と並列化(parallelization)がある。
 ベクトル化 --- 複数の値の集まりであるベクトルのように、複数のデータを一括して扱う
           CPUのSIMD(Single Instruction Multiple Data)命令を発生する
           マルチコアの必要はない
           Visual C++のデフォルトであるQvecはこのタイプである
 並列化 ------ マルチコアCPUを利用して複数のスレッドで並列化するもので、QparやOpenMPは、このタイプである
            ベクトル化と併用して付加的に使用される

ここでの比較方法
 以下に示す全ての組み合わせ(24通り)について実行時間を調べる。
     ビルド較正 ------- Debug、Release
     forループ -------- 一重、二重
     処理内容 --------- 単純代入、累加算代入
     並列化手法 ------- Visual C++のデフォルト(Qvac)、Qparの付加、OpenMPの付加
 累加算代入処理に関しては、演算結果を表示させ、誤計算が生じていないことを確認する。

Visual StudioのQparコンパイラオプションを設定する
 Qparオプションを設定するには、下記の手順を用いる。
 1)メニューから「プロジェクト」→「*****のプロパティ」を選択する(*****はプロジェクト名)。
 2)開いた「*****プロパティページ」で、
    左欄 --- 「構成プロパティ」→「C/C++」→「コード生成」
    右欄 --- 「並列コード生成の有効化」を選択して右の「V」をクリックし、「はい(/Qpar)」を選択
   とし、「OK」をクリックする。
  なお、ビルド条件のDebug構成とRelease構成のそれぞれについて個別に行う必要がある。
 図1は、Release構成で、プロパティページを開き、「並列コード生成の有効化」で「はい(/Qpar)」を設定したところを示す。



図1 「はい(/Qpar)」を選択して、「OK」をクリックする直前のプロパティページ


一重ループで代入処理をするプログラムを用いて、各並列化手法を比較する
 簡単な一重forループを用いて、三角関数の計算結果を配列に代入して、いろいろな並列化の効果を確認する。 Qparを用いるコードと、OpenMPのコードを同時に記述すると、「OpenMPディレクティブと#pragma loop(hint_parallel)を同じ関数(すなわち、main)内で使用することはできません」と言われるので、Qparの場合とOpenMPの場合のプログラムを分けて作成した。
◎Visual C++のデフォルトとQparの付加の比較
 プログラムを図2に示し、Debug構成でビルドした結果を図3に、Release構成での結果を図4に示す。


図2 Visual C++のデフォルトとQparの付加を比較するプログラム



図3 Debug構成では、Visual C++のデフォルトとQparの付加にはほとんど差がない



図4 Release構成では、Visual C++のデフォルトよりQparに付加が劣る


◎Visual C++のデフォルトとOpenMPの付加の比較
 プログラムのOpenMPの部分を図5に示し、結果を図6と図7に示す。#include <omp.h>が必要である。


図5 OpenMPの付加との比較のために書き換えた部分



図6 Debug構成では、Visual C++のデフォルトよりもOpenMPの付加の方が優れている



図7 Release構成では、Visual C++のデフォルトよりもOpenMPの付加が劣る


一重ループで代入処理をするプログラムで比較して分かったこと
 1)Debug構成では、Visual C++のデフォルトよりも、OpenMPによる並列化が優秀である。
 2)Release構成では、逆に、Visual C++のデフォルトが著しく能力を発揮し、OpenMPが劣った。
 3)速い順に並べると、Release(デフォルト)→(大きく離れて)→Release(Qpar)→Release(OpenMP)→Debug(OpenMP)→Debug(デフォルト)、Debug(Qpar)となる。
 結論として、ここで試みた例に関する限り、Relase構成でのVisual C++のデフォルトが非常に優れており、OpenMPやQparを付加する必要がない。Debug構成ならば、OpenMPを使用する価値がある。

二重ループで代入処理をするプログラムを用いて、各並列化手法を比較する
 一重ループの場合の代入先のa[i]を、二次元配列a[i][j]に替えて、二重ループ化した。
◎Visual C++のデフォルトとQparの付加の比較
 使用したプログラムの主要部を図8に、結果を図9と図10に示す。


図8 二重ループを用いた代入を、Visual C++のデフォルトとQparの付加とで比較するプログラム



図9 Debug構成では、Visual C++のデフォルトとQparの付加に差がない



図10 Release構成でも差がない


◎Visual C++のデフォルトとOpenMPの付加の比較
 使用したプログラムを図11に、結果を図12と図13に示す。
 OpenMPの場合、二重ループの内側の変数に対しては、
    #pragma omp parallel for private(j,)
と記述して、並行して実行される他スレッドとの混乱を避ける必要がある。


図11 OpenMPの付加と比較するために書き換えた部分



図12 Debug構成では、Visual C++のデフォルトよりも、OpenMPの付加が優れている



図13 Release構成では、Visual C++のデフォルトよりも、OpenMPの付加が著しく劣る場合もある


二重ループで代入処理をするプログラムで比較して分かったこと
 速い順に並べると、Release(デフォルト)、Release(Qpar)→(大きく離れて)→Release(OpenMP)→Debug(OpenMP)→Debug(デフォルト)、Debug(Qpar)となる。
 結論として、ここで試みた二重ループによる代入処理に関する限り、Relase構成で、Visual StudioデフォルトとQparの付加は同等で優れており、OpenMPは著しく劣る。Debug構成では、OpenMPがVisual C++のデフォルトよりも優れている。

一重ループと二重ループで累加算代入処理をするプログラムを用いて、各並列化手法を比較する
 三角関数の計算結果を累加算代入して、合計を求めるプログラムを、一重ループと二重ループで実行する。
 プログラムの要点のみを、図14に示す。追加する並列化手法に応じて、異なったコンパイラ指示文を記述する(#pragmaで)。


図14 比較のために用いた、ループ演算の主要部


◎Visual C++のデフォルトとQparの付加の比較
 Visual C++のデフォルトに対しては、コンパイラ指示文を必要としない。Qparに対しては、一重と二重のそれぞれに、
    #pragma loop(hint_parallel(4))
と記述する。hint_parallel(n)は、「n個のスレッドが使えるよ」とコンパイラにヒントを与えるものである。ここでヒントとしたのは、並列化の指示ではなく、「並列化可能な条件が満足されていれば、並列化せよ」の意味だからである。あらかじめスレッド数が分かっているので4を入れたが、不明の場合は0とする。その場合は、使用可能なスレッド数の最大値が用いられる。
 結果を図15と図16に示す。


図15 Debug構成では、Visual C++のデフォルトとQparの付加は性能にほとんど差がない



図16 Release構成でも、Visual C++のデフォルトとQparの付加は性能にほとんど差がない


◎Visual C++のデフォルトとOpenMPの付加の比較
 Visual C++のデフォルトに対しては、コンパイラ指示文を記述しない。OpenMPに対しては、一重の場合は、
    #pragma omp parallel for reduction(+:sum_single)
とし、二重の場合は、
    #pragma omp parallel for reduction(+:sum_double) private(j, i_100)
と記述する。
 reduction(+:sum_double)は、別々のスレッドで並行して計算したsum_doubleを、さらに加算(+)して、まとめ(reductionする)て最終的なsum_doubleとすることを指示する。private()は、すでに説明した通り、内部ループの変数が各スレッド固有であることを指示する。
 結果を図17と図18に示す。


図17 Debug構成では、OpenMPは、一重ループで約60%、二重ループで約40%に短縮させる



図18 Release構成では、OpenMPは、一重ループで約65%、二重ループで約30%に短縮させる


一重ループと二重ループで累加算代入処理をするプログラムを用いて、各並列化手法を比較して分かったこと
 一重ループの場合で、速い順に並べると、Release(OpenMP)→(大きく離れて)→Release(Qpar)、Release(デフォルト)→Debug(OpenMP)→Debug(デフォルト)、Debug(Qpar)となる。
 二重ループの場合を速い順に並べると、Release(OpenMP)→(非常に大きく離れて)→Debug(OpenMP)→Release(Qpar)、Release(デフォルト)→Debug(Qpar)、Debug(デフォルト)となる。
 結論として、ここで試みた累加算代入処理に関する限り、OpenMPの付加が圧倒的な強みを見せている。Qparの付加はVisual C++のデフォルトと同等で、使用する理由は特に見当たらない。

実行時間のバラツキ現象
 実行時間は、実行の都度変わる。特に、初回は非常に時間がかかる場合がある。この現象は、Visual C++のデフォルト(Qvec)で多く見られた。この原因として、キャッシュメモリがあり、ヒットの具合で、実行時間が大きく異なる。最初は、キャッシュにメインメモリから持ってくるのに時間がかかる(キャッシュのcold start現象)。実行時間の比較の際、プログラム上の順序として、先にVisual C++のデフォルトを実行し、続いて、コンパイラ・オプションを付加したものを実行した影響が出ているのかもしれない。
 メモリアクセスの待ち時間が重要で、メモリのバンド幅が影響する場合もある。
 ネットに接続されていると、気が付かないで、メール着信などの割り込みが生じていることもある。
 信頼でき、意味のある実行時間を求めるには、安定した状態(steady state)での数百回以上の繰り返しの平均が必要である、とも言われている。
 この稿での実行結果は、10数回の繰り返し実行の後に、ほぼ安定したと思われる状況を示してある。

結 論
 今回調べた条件に限っての結果であるが、単純な代入処理に対しては、Release構成では、一重ループでも二重ループでも、Visual C++のデフォルト(Qvec)で十分であり、QparやOpenMPの付加は必要ない。Debug構成では、一重、二重共に、OpenMPの付加がVisual C++のデフォルトに勝り、Qparの付加はVisual C++のデフォルトと同等で、有意義と思われなかった。
 累加算代入処理になると、Debug構成はもちろんのこと、Release構成でも、OpenMPの威力が発揮された。特に、二重ループの場合には、OpenMPで、Visual C++のデフォルトの3倍の速度(実行時間で1/3)が得られた。
 結局、Debug構成なら無条件でOpenMPを使い、Release構成ならば、単純な代入処理のループにはOpenMPを付加してもほとんど改善効果がなく、確率的には、かえって悪くなる場合もあり、累加算代入などのより複雑な処理のループではOpenMPを付加すべき、となる。Qparを付加する理由は、特に見当たらなかった。

参考資料
"Auto-Parallelization and Auto-Vectorization"
http://msdn.microsoft.com/en-us/library/hh872235.aspx

"Using Automatic Vectorization"
http://software.intel.com/en-us/node/522572

"Loop Constructs"
http://software.intel.com/en-us/node/522574





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