並列ループ
並列処理で有名なものでループ処理があります。ループ処理はプログラムを繰り返して実行するので、比較的並列化しやすいプログラムです。それでも、人が記述するとミスをしやすく、バグを発見するのは難しいものです。そこで、並列的なループ処理を記述する必要があると言えます。
幸いOpenMPには並列的なループ処理を比較的簡単に記述するためのループ構文が用意されています。これからサンプルプロジェクトomp_forをもとにループ構文を解説します。
#include <stdio.h> #include <omp.h> int main() { int i; /* 正式な書き方(パラレル構文とループ構文) */ #pragma omp parallel { #pragma omp for for ( i = 0; i < 5; i++ ) printf( "インデックス%d:スレッド番号%d\n", i, omp_get_thread_num() ); } printf( "\n" ); /* 簡潔な書き方(パラレルループ構文) */ #pragma omp parallel for for ( i = 0; i < 5; i++ ) printf( "インデックス%d:スレッド番号%d\n", i, omp_get_thread_num() ); printf( "\n\n" ); return 0; }
![](http://cz-cdn.shoeisha.jp/static/images/article/4786/omp_for.gif)
基本的なループ構文は#pragma omp forをfor文の前にコーディングするだけです。ただし、パラレル構文内にコーディングする必要があることに気をつけてください。しかしながら、ループ構文しか使用しない場合、パラレル構文をコーディングするのは面倒です。その場合、for文だけを並列化したい場合は、パラレルループ構文を使用するとプログラムが見やすくなります。
サンプルプロジェクトomp_forを実行してみてください。ループ処理を複数のスレッドが分担して処理している様子が確認できます。
文法そのものは簡単なのですが、ループする内容を良く考えて実装しないと、逐次プログラミングではなかったエラーが生じます。
そのことを示すためのサンプルプロジェクトomp_for_error用意しましたので見てください。
#include <stdio.h> #include <omp.h> #define MAX 100000 int main(void) { int i, errorCount; int value[ MAX ], value1[ MAX ]; /* 逐次的にループ処理を行う */ for( i = 0; i < MAX; i++ ) { if ( i > 2 ) value[ i - 2 ] = i; value[ i ] = i; if ( i < ( MAX - 2 ) ) value[ i + 2 ] = i; } /* 並列的にループ処理を行う */ #pragma omp parallel for num_threads(8) for( i = 0; i < MAX; i++ ) { if ( i > 2 ) value1[ i - 2 ] = i; value1[ i ] = i; if ( i < ( MAX - 2 ) ) value1[ i + 2 ] = i; } /* 処理結果の違いを調べる */ errorCount = 0; for( i = 0; i < MAX; i++ ) { if ( value[ i ] != value1[ i ] ) { printf( "インデックス%dの値が違います(逐次%d:並列:%d)\n", i, value[ i ], value1[ i ] ); errorCount++; } } printf( "処理結果が%d個違います。\n", errorCount ); /* 終了 */ printf( "\n" ); return 0; }
![](http://cz-cdn.shoeisha.jp/static/images/article/4786/omp_for_error.gif)
このサンプルプログラムの生成するスレッド数を指定するnum_threadsの部分を変更しつつ実行してみてください。スレッド数が1ならば逐次処理と同じ処理結果になり、スレッド数が多くなればなるほどエラーが生じやすいことが確認できます。
このサンプルでエラーが生じる理由は「処理範囲が重なっているから」です。並列ループで処理する場合、複数のスレッドが同じ領域を読み書きしないように注意してください。処理範囲が重なってしまうと、スレッドの実行順序は保証されませんので計算結果が毎回異なってしまい、逐次処理では予想もつかないエラーが生じることになってしまいます。
重なっている場所はvalue1[ i - 2 ] = i;
・value1[ i ] = i;
・value1[ i + 2 ] = i;
の部分です。例えば変数iが2の場合、配列value1の0・2・4の部分を書きかえています。このコードは、逐次処理の時は値がインデックス値が大きい方で上書きされるので最終的な処理結果が、value1[ i ] = i + 2;
(value1[ 2 ] = 4)だと予測できます。しかし並列処理の場合スレッドの実行順序は決まっていませんので、最終的にどのインデックスでの処理が値を上書きするのか分かりません。 先ほどの例を使い説明すると、変数iが4の時の処理が、変数iが2の時の処理よりも先に実行されて、value1[ i ] = i;
(value1[ 2 ] = 2)になってしまうかもしれません。この様に並列処理では、処理範囲が重ならないように慎重に設計とコーディングをする必要があります。具体的な方法についてはこの回以降に解説していきます。
これでループ構文の基本的な書き方の説明が終わったので、どれだけパフォーマンスが向上するのかについて解説します。パフォーマンスの向上を測定するのは、やはり実際にプログラムを動かすのが一番です。サンプルプロジェクトomp_for_testを実行してみてください。
#include <stdio.h> #include <omp.h> #include <windows.h> int main() { int x, y; int count, time; double start, end, diff; double omp_for_start, omp_for_end, omp_for_diff; for ( x = 1; x <= 5; x++ ) { /* 処理回数および処理時間を設定&表示 */ time = 1 * x; count = 100 * 2 * x; printf ( "[実験%d回目】処理回数{%d}:一処理当たり{%d}ミリ秒\n", x, count, time); /* 逐次的に何かの処理をする */ start = omp_get_wtime(); for ( y = 0; y < count; y++ ) { Sleep( time ); } end = omp_get_wtime(); diff = end - start; /* 並列的に何かの処理をする */ omp_for_start = omp_get_wtime(); #pragma omp parallel for for ( y = 0; y < count; y++ ) { Sleep( time ); } omp_for_end = omp_get_wtime(); omp_for_diff = omp_for_end - omp_for_start; /* 測定結果を表示 */ printf( "逐次処理の処理時間{%f}秒\n", diff ); printf( "並列処理の処理時間{%f}秒\n", omp_for_diff ); printf( "処理効率は[%f】倍です。\n", ( diff / omp_for_diff ) ); printf( "\n" ); } printf( "\n\n" ); return 0; }
![](http://cz-cdn.shoeisha.jp/static/images/article/4786/omp_for_test.gif)
このサンプルプロジェクトでは、1ループあたりにかかる処理時間と処理回数を増加させつつ処理時間を計測しています。このサンプルを実行させると、1ループあたりの処理時間と処理回数を増加させるにつれパフォーマンスが向上していることと、ループ構文の高い処理効率を確認できます。
ただし、高い処理能力だと言ってもCPUのコア数と同じ倍率にはならないことに注意してください。並列化を実現するための処理が必要であるため、処理効率はほとんどどの場合コア数以下となります。それと、このサンプルは単純な処理しかしていないため処理効率が高くなっているので注意してください。複雑な処理をループ内にコーディングすると、これほどパフォーマンスは向上しません。
実務でループ構文を使用する際には、このサンプルをもとに処理効率を実際に測定するプログラムをコーディングするか、市販の並列処理に対応した開発ツールを使用してパフォーマンスをよく確認してください。
CPUのキャッシュの効果が影響し、時々マルチコアCPUの処理効率が論理コア数倍以上になることがあります。ですが、並列処理には必ず並列化するための処理が必要ですので、コア数よりも処理効率が上がることはほとんどどありえません。その様な実験結果が出ても、実稼働環境で同じパフォーマンスが達成できないことの方が多いです。注意してください。
おわり
今回は実行時ライブラリの使い方と並列ループ処理について解説しました。並列ループについては、変数の扱いについても知る必要がありますが、変数の取り扱いは色々な要素の解説が必要になりますので、今回は紹介しませんでした。次回以降で変数やより高度な概念について解説します。
参考資料
- Visual C++のOpenMP(MSDN)
- OpenMP公式ホームページ
- 『OpenMP入門』 北山洋幸 著、秀和システム、2009年8月