ParallelReduceSampleの並列処理について
ParallelReduceSampleはparallel_reduce
テンプレートを使用しています。parallel_reduce
テンプレートを使用するために、並列処理を行うオブジェクトは4つの条件を満たさねばなりません。
1つ目は、専用のコピー・コンストラクター(インテルTBBでは「分割コンストラクター」と呼びます)を実装することです。普通のコピー・コンストラクタと区別するためにtbb::split
クラスを指定しなくてはなりません。
2つ目は、デストラクターを用意することです。
3つ目は、operator()
を多重定義することです。
4つ目は、join
というメソッドを定義しなくてはなりません。詳細は後でサンプルの解説と共に行います。
ParallelReduceSampleの並列処理部分は下記の通りです。
/*----------------------------------------------------------------- 並列的に分析値を算出するための設定です ----------------------------------------------------------------*/ //分割コンストラクター Sales( Sales& obj, split ) : name( obj.get_name() ), price( obj.get_price() ), datas( obj.get_details() ), count( obj.get_detailCount() ), sumAmount( 0 ), sumTax( 0 ), sumCount( 0 ), maxCount( INT_MIN ), maxIndex( -1 ), minCount( INT_MAX ), minIndex( -1 ), aveCount( 0 ), aveAmount( 0 ){}; //計算結果を結合するためのメソッド void join( const Sales& obj ) { //合計を算出 this->sumAmount += obj.get_sum(); this->sumTax += obj.get_sumTax(); this->sumCount += obj.get_countSum(); //最大値と最小値を決定 if ( this->maxCount < obj.get_maxCount() ) { this->maxCount = obj.get_maxCount(); this->maxIndex = obj.get_maxIndex(); } if ( this->minCount > obj.get_minCount() ) { this->minCount = obj.get_minCount(); this->minIndex = obj.get_minIndex(); } }; //範囲内で各種計算を行います void operator() ( const blocked_range<size_t>& range ) { SalesDetails* tmp = this->datas; for ( size_t i = range.begin(); i != range.end(); i++ ) { //合計を算出 this->sumAmount += tmp[ i ].get_amount(); this->sumTax += tmp[ i ].get_consumption_tax(); this->sumCount += tmp[ i ].get_count(); //最大値もしくは最小値を決定 if ( this->maxCount < tmp[ i ].get_count() ) { this->maxCount = tmp[ i ].get_count(); this->maxIndex = i; } else if ( this->minCount > tmp[ i ].get_count() ) { this->minCount = tmp[ i ].get_count(); this->minIndex = i; } } };
分割コンストラクターでは、初期化リスト内で渡されたオブジェクトの任意の値をコピーし、分析によって導出する値(最大値など)については初期化しています。このコンストラクターは、parallel_reduce
テンプレートがオブジェクトを分割する時に呼び出します。この時点ではまだ分析値(総売上、最大金額など)は導出されていませんので初期値に設定しています。
operator()
の多重定義では、売上金額・税金額・商品数の各項目を加算し、それと同時に最小値と最大値を割り出しています。この時点では、分割されたオブジェクト内での計算課程ですので、全体としての合計値や最大/最小値が割り出されているわけではないことと、今回はconst
が指定されていないことに注意して下さい。このテンプレートでは変更が予想されますのでconst
を定義してはなりません。ここは間違えやすい点ですので注意して下さい。
全体としての各種値を出すのは、join
メソッドの仕事です。parallel_reduce
テンプレートは最初に、対象となるオブジェクト(このサンプルではSales)を分割していきオブジェクトのコピーを作ります。分割が終わったらその範囲内で処理を行います。そして最後に、各コピーしたオブジェクトのjoin
メソッドを呼び出して、2つのオブジェクトを結合していき、最終的に1つのオブジェクトになるまでそれを繰り返します。join
メソッドの引数が自分と同じオブジェクト(このサンプルではSales)なのはこれが理由です。
ParallelReduceSampleを実行して下さい。並列処理の処理効率が良いことが確認できます。
しかし、parallel_reduce
テンプレートを使う上で注意するべき点があります。それは、演算の丸め誤差です。並列処理では処理の実行順序がバラバラで、割り算の計算結果が毎回違います。
例えば、100/4/3/2を計算するとします。直列では毎回(((100/4)/3)/2)の順序で計算されます。しかし、並列では((100/4)/(3/2))の様に計算されます。従って、丸め誤差により計算結果が同じではなくなります。並列プログラミングを初めて行った際によく起こる過ちですので十分に注意して下さい。
また、parallel_reduce
テンプレートは結果を結合する処理がありますので、どうしてもparallel_for
テンプレートよりも低くなります。ですから極力parallel_for
テンプレートを利用するとよいでしょう。
並列ループの考え方
今回2つのテンプレートを通じて、ループ処理を並列化する方法について解説しました。並列化に慣れていない人はあまりピンと来ないと思いますが、考え方そのものは非常に単純です。ループの処理は分解して考えると、次のように処理をしていることになります。
//処理1 tmp[ 0 ].set_amount( this->price * tmp[ 0 ].get_count() ); tmp[ 0 ].set_consumption_tax( tmp[ 0 ].get_amount() * 0.05 ); tmp[ 0 ].set_total( tmp[ 0 ].get_amount() + tmp[ 0 ].get_consumption_tax() ); //処理2 tmp[ 1 ].set_amount( this->price * tmp[ 1 ].get_count() ); tmp[ 1 ].set_consumption_tax( tmp[ 1 ].get_amount() * 0.05 ); tmp[ 1 ].set_total( tmp[ 1 ].get_amount() + tmp[ 1 ].get_consumption_tax() ); //以下省略
この処理をよく見ると、各々の処理が他の処理に依存せずに、限られた範囲内で処理をしています。ということは、マルチコアプロセッサならば、処理1と処理2が同時に行えると考えるのは自然です。これがparallel_for
テンプレートの考え方です。
次に、ParallelReduceSampleの様に要素全体で処理をする場合、タスクを分割してその範囲内で最大値などを求め、後でその結果を突き合わせて判断をするようにすれば、マルチコアプロセッサで同時に複数のタスクが処理できることになります。これが、parallel_reduce
テンプレートの考え方です。
この2つの考え方を改めて考えると、普段我々人間がしている仕事と同じです。私たちは多くの場合、1つのプロジェクトの作業を分割して複数の人間で作業しますし(parallel_for
テンプレートと同じ)、各プログラマーが複数のオブジェクトをプログラミングしてからそれを統合して1つのシステムを実装します(parallel_reduce
テンプレートと同じ)
こうしてよく考えてみれば、並列プログラミングもそれ程特殊な考え方でないことが分かって頂けると思います。
まとめ
今回は、インテルTBBのparallel_for
テンプレートとparallel_reduce
テンプレートの使い方を解説しました。また、その根底に流れる「ループ処理を並列化する」という考え方も解説しました。今まで並列プログラミングに対して苦手意識をもっている方は多いと思います。
しかし、今回の記事を読めば、並列プログラミングの考え方がそれ程特殊なものではなく、普段我々人間がしていることと大差がないことが分かって頂けると思います。
並列プログラミングは今後当たり前のものとなります。少しでもその準備のお手伝いができれば幸いです。次回は、より高度な並列プログラミングの概念を噛み砕いて解説します。お楽しみに。
参考資料
書籍
- 『インテル スレッディング・ビルディング・ブロック』 James Reinderss著、菅原清文・エクセルソフト 訳、オライリー・ジャパン、2008年2月26日