インテルTBBの決まりごと
インテルTBBを使用するプログラムには、いくつかの決まりごとがありますので、それを最初に解説します。まず1つ目に、必ずインテルTBB使用前にtask_scheduler_init init
オブジェクトを初期化せねばなりません。このオブジェクトを初期化しないと、インテルTBBは正常に動作しません。
次にtask_scheduler_init init
オブジェクトが必要ですので、それに伴いインテルTBBを使用するプログラムは#include "tbb/task_scheduler_init.h"
が必要となります。
最後に標準テンプレート・ライブラリー(STL)のコンテナはインテルTBBと一緒に使用しないで下さい。STLのコンテナは並列化を意識したものではないので、インテルTBBと一緒に使うと壊れてしまいます。vector
などのSTLコンテナを使用せずに、今後紹介するインテルTBB用のコンテナを使用して下さい。
ParallelForSampleの並列処理について
このサンプルでは、処理を並列化するためにインテルTBBのparallel_for
テンプレートを使用しています。parallel_for
テンプレートはループを並列化するためのもので、並列化する処理が互いに独立している場合に使用します。ループ内の処理が互いに干渉しあう場合に使用すると正しい結果を得られませんので注意して下さい。
parallel_forを正常に動作させるためには3つの要件を満たさねばなりません。1つ目は、コピー・コンストラクターを用意する必要があるということです。2つ目にデストラクタがなければなりません。3つ目に、operator()
「関数呼び出し演算子」を多重定義しなくてはなりません。コピー・コンストラクターとデストラクターは、コンパイラが自動で用意してくれますが、これは自分で定義しなくてはなりません。
関数呼び出し演算子にあまり馴染みがない方は、サンプルFunctionCall
のコードを読んで実行して見て下さい。そうすれば、関数呼び出し演算子の定義方法と大まかな動作が分かると思います。
#include <iostream> #include <tchar.h> using namespace std; class FunctionCall { int value; public: //関数呼び出し演算子 void operator()( int param ) { cout << "関数呼び出し演算子が呼ばれました。" << "値は" << param << "です。" << endl; }; //constを指定した関数呼び出し演算子 void operator() () const { //下記プログラムのコメントを外すとエラー //value = 0; }; }; int _tmain(int argc, _TCHAR* argv[]) { FunctionCall obj; obj(1); cout << endl << endl; return 0; }
インテルTBBでは関数呼び出し演算子の多重定義を多用します。慣れていない方は、関数呼び出し演算子の多重定義方法を身につけておくことをお勧めします。
本題に戻ります。今回のParallelForSampleでは関数呼び出し演算子を多重定義して、直列のものとほぼ同じ計算プログラムをコーディングしているだけです。比較して見ると定義方法以外はプログラムがほぼ同じです。
//直列処理 void Calculate( SalesDetails* datas, int count ) { for ( int i = 0; i < count; i++ ) { datas[ i ].set_amount( this->price * datas[ i ].get_count() ); datas[ i ].set_consumption_tax( datas[ i ].get_amount() * 0.05 ); datas[ i ].set_total( datas[ i ].get_amount() + datas[ i ].get_consumption_tax() ); } this->datas = datas; this->count = count; } //並列処理 void operator() ( const blocked_range<size_t>& range ) const { SalesDetails* tmp = this->datas; for ( size_t i = range.begin(); i != range.end(); i++ ) { tmp[ i ].set_amount( this->price * tmp[ i ].get_count() ); tmp[ i ].set_consumption_tax( tmp[ i ].get_amount() * 0.05 ); tmp[ i ].set_total( tmp[ i ].get_amount() + tmp[ i ].get_consumption_tax() ); } };
operator()
にconst
が指定されている点に注意して下さい。parallel_for
は内部で、処理範囲を分割し、その分割した範囲内に指定されたオブジェクトのコピーを作成して、operator()
を呼び出します。この時にもし変更を許してしまうと、複数のコピーを矛盾なく並列処理させることが困難になります。そういった理由がありますので、対象となるオブジェクトのデータメンバーを変更することができません。そこで変更できないようにconst
を必ず指定せねばなりません。この点に注意して下さい。
もしconst
を指定すると、どのような動きをするのか確認したい場合は、FunctionCallプロジェクト内のコメントを取り除いてみて下さい。そうすればコンパイラエラーが発生します。つまり、parallel_for
は誤りを事前に防いでくれるわけです。
今回のサンプルのoperator()
内でSalesDetails* tmp = this->datas;
のプログラムがあるのは、const
が定義がされているからです。const
が定義されている時に直接メンバーを変更することはできませんが、間接的に変更することはできます。それでこのプログラムで間接的に変更しているのです。間接的に変更するだけならば、複数のコピーの値を同じにすることができるので安全です。
これで呼ばれるオブジェクトの準備は終わりです。後はparallel_forでそのオブジェクトを呼ぶだけです。呼び出し方は簡単で、parallel_for( blocked_range( 0, count, 1000 ), obj );
のようにparallel_for
テンプレートで、blocked_range
オブジェクトを初期化したものと、並列処理をするオブジェクト(今回はSales
)のインスタンスを指定するだけです。
/* 売上明細で算出するべき項目を【並列で】計算します。 */ void ParallelCalculate( Sales* target, SalesDetails* datas, int count ) { Sales obj( target->get_name(), target->get_price() ); obj.set_details( datas ); obj.set_detailCount( count ); parallel_for( blocked_range<size_t>( 0, count, 10000 ), obj ); //ここに注目 };
blocked_range
オブジェクトは、範囲を表すオブジェクトです。開始地点・繰り返し回数・粒度を指定します。粒度を簡単にいうと、処理を分割する基準となる値です。この値を変更することにより並列処理のパフォーマンスが変化します。粒度については高度な概念なので今回は10000にするとよいと考えて下さい。
これでParallelForSampleの説明は終わりです。早速実行してみてください。そうすると、直列処理よりも並列処理の方がかなり効率が良いことを体験できます。
ただし、一つ注意するべき点があります。それはスピードは一定ではなくある程度の幅がある点です。実務でインテルTBBを使用する際には、何度か実行して処理効率を確認して下さい。
次項では少し複雑な並列化処理を採り上げます。