2016. 3. 8..
石立 喬

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

――― より長い処理を、多数回測定して、OpenMPの効果を確認する -----

 OpenMPを使用しての実行時間にはバラツキが大きかった。この原因は、測定対象の処理時間に比べて、オーバーヘッドやキャッシュミスなど不規則な変動時間の割合が大きかったからと考えられる。
 そこで、今回はforループの繰り返しを1000000回と大きくして、処理の所要時間を増やし、同じ測定を100回繰り返して、Visual C++のデフォルトとOpenMPの付加とを比較した。

調査方法
 処理内容 ---- 配列への代入と累加算代入
 ビルド構成 --- DebugとRelease
のすべての組み合わせ(4種類)に対して、
     1)一重ループ ------ 一変数の三角関数の積を、1000000回
     2)二重化ループA --- 上記の演算を、1000回 X 1000回に二重化する
     3)二重化ループB --- 上記で、i * 1000 の演算を効率化する
の実行を、いずれか単独でそれぞれ100回繰り返し、その都度の実行時間を測定した。これを、Visual C++のデフォルトと、OpenMPを付加した場合とについて調べた。

配列への代入
 使用したプログラム(一重ループの場合)を図1に示す。時間の測定方法は、従来用いてきたものと同じで、28行のstart_timeの設定と、34行のend_timeの設定の間に、一重ループの処理を記述し、所要の実行時間elapsed_timeを求める。
 時間測定は100回繰り返して行い、1回目と10の倍数の回のみを表示する。100回すべての測定結果をaccumulated_timeに足し込み、最後に100で割って、平均実行時間とする。


図1 配列への代入を一重ループで行う


比較に用いた三種類のループ

 図1の30行〜32行に示したように、sin関数とcos関数の積を一次元配列に入れるものを「一重ループ」と呼び、さらに、表1に示す「二重化ループA」、「二重化ループB」を書き換えて比較した。表はOpenMPを用いる場合を示しているが、Visual C++のデフォルトを使う場合には、#pragma文をコメントアウトする。
 「二重化ループA」は、「一重ループ」を単純に二次元化して二重ループに分解したものであり、「二重化ループB]は、変数i_1000を用いて、内部ループで、i * 1000の乗算を繰り返す無駄を省いたものである。いずれも、最終的に代入されるのは、一次元配列である。

 表1 使用したループ

 一重ループ 
 二重化ループA 
 二重化ループB


配列への代入で得られた結果
 図1のプログラムをDebug構成で実行した「一重ループ」の結果を図2に示す。他のループの場合も、実行時間は異なっても画面上への出力形式は同じなので、画面での紹介は省略し、図3〜図5に結果を表とグラフで示す。


図2 一重ループの実行速度を求めたときの出力画面


◎一重ループ
 結果を図3に示す。Debug構成の場合には、OpenMPによる速度向上効果が大きい(40%〜17%)が、Releasr構成では、OpenMPの効果が60%程度にとどまる。


図3 一重ループでは、Release構成で、OpenMPによる短縮は60%程度


◎二重化ループA
 結果を図4に示す。Debug構成、Release構成ともに、OpenMPによる速度向上効果が大きい(40%〜17%)。実行時間の絶対値の平均値で見ると、Debug構成では、VC++もOpenMPも、一重の場合と同じで、あえて二重化する必要がない。Release構成では、VC++の場合は、著しく悪化(実行時間が3倍に伸びる)し、OpenMPの場合は、ほぼ同じである。
 結局、図3と図4のグラフを比較して分かるように、Release構成のVC++のみが悪化し、他は、一重ループの場合と変わらなかった。


図4 二重化ループAでは、Release構成のVC++(遅くなる)以外は一重ループと変わらない


◎二重化ループB
 結果を図5に示す。二重化ループ処理Aの場合に比べて、平均値で比較すると、Debug構成の場合、VC++もOpenMPも、ほぼ同じであった。ところが、Release構成ではVC++で160%と著しく悪化し、OpenMPでも140%と増加した。VC++の場合で見ると、Debug構成とRelease構成で実行速度に変わりがない。
 i * 1000の演算の代わりに、i_1000を代入する方が、高速化になるとの狙いであったが、Debug構成では改善が見られず、ほぼ同じで、Release構成では、かえって悪くなった。


図5 二重化ループBでは、Debug構成は一重ループと同じ、Releaseは悪化


配列への代入処理のまとめ
 
バラツキの多い一回目ではなく、100回の平均で比較したのが表2である。これによれば、一次元配列に代入する、「一重ループ」、「二重化ループA」、「二重化ループB」の実行時間は、Debug構成では、誤差の範囲内で完全に一致した。i * 1000の計算時間は、他の処理時間に比べて微々たるもので、ほとんど影響しなかったものと思われる。
 しかし、Release構成では、大きく異なった。 表2の着色した欄が異常な値を示している。VC++の場合、「二重ループA」で「一重ループ」の3.4倍、「二重ループB」で4.7倍に実行時間が増大する。OpenMPの場合でも、「二重ループB」が1.4倍になる。再度繰り返しても同じ結果になり、原因は良く分からない。Release構成の場合は、一重ループは、二重化しないで、そのまま使用すべきとの結論になる。

表2 配列代入処理の実行時間比較(単位はmsec)



総和sumへの累加算代入処理
 プログラムで変更したのは、
 1)配列への代入 a[i] = の代わりに、総和sumに累加算代入するsum +=を用いる(sumの初期値を0に設定して)。
 2)#pragma文の最後に、reduction(+:sum)を追加する。
である。
 時間の測定方法、処理の繰り返し回数は全く同じである。

累加算代入処理で得られた結果のまとめ
 結果は、すべてのループ処理で、ほとんど同じであった。配列への代入は、1000000個のアドレスから、その都度異なるアドレスを選択して書き込むのに対し、累加算代入は、いつも決まったアドレスのsumを読み取り、加算演算後、再び元のアドレスに書き込むので、キャッシュ効果が期待できる。加算による時間増大が、キャッシュの有効利用で相殺されて、実行時間が同じになったものと想像する。
 配列への代入の場合の表2に相当するのが表3である。これによれば、Debug構成の場合は、一次元配列に代入する三種のループに関しては、表2と一致する。Release構成では、VC++が配列代入よりも時間がかかり、OpenMP使用では配列代入と変わらない。

表3 累加算代入処理の実行時間比較(単位はmsec)



結 論
 ループ回数を、従来の1000回から1000000回に増やし、オーバーヘッドなどの変動要因を減らして、実行時間を計測した。同じ処理の測定を100回繰り返し、推移を調べた。
 ここでの結論は、実際に使用しての感覚と異なる恐れがある。実際には、同じ処理を100回も繰り返し実行し、実行時間が短縮されるまで待つことはしない。また、一回の実行に数10msec以上かかる処理も多くない。
 第一回目の実行時間に大きいバラツキがあった(ここでは、データを具体的に示していないが)ことは、それまでのキャッシュメモリの内容などに大きく影響されることを示している。
 総実行回数が同じ(この場合は1000000回)であれば、Debug構成の場合は、一重ループ、それを二重化した二種類のループ、そして、一次元配列への単純代入と累加算代入の二種類の処理、これらすべての実行時間は平均ではほぼ同一と言う、当然のような、意外なような結果になった。OpenMP使用による改善は、もちろん認められた。
 ただし例外があり、Release構成における配列への代入で、VC++の場合、二重化ループAで一重の約3.4倍、二重化ループBで約4.7倍の時間がかかった。OpenMPを用いた場合では、二重ループAは一重ループと変わらず、二重ループBでは40%増えた。Release構成では、なるべく、一重ループで使うべきとの結論になった。
 「OpenMPとVisual C++による並列処理(1)」や「同(2)」の結果と異なる部分もあるが、両方とも正しいと思っている。OpenMPの効果は、それくらい、状況に左右され易いものである。実際の使用に当たっては、処理内容に応じた調査を行い、処理方法の選択が必要であろう。








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