並列処理が有効な典型的な例として、配列や各種コンテナに格納されたデータに対し、始点と終点を指定して次々と反復処理を行う、というパターンがある。リスト2はその典型的なパターンで、要素数がそれぞれdimである配列aと配列bに対し、おのおのの要素を足し合わせる処理をするものだ。
リスト2 典型的な反復処理の例:配列どうしの加算
void VectorAdd( double* a, double* b, int dim ) {
for( int i = 0; i dim; i++ ) {
a[i] = a[i] + b[i];
}
}
このような反復処理を実装したアルゴリズムが、TBBの「parallel_for」テンプレート関数だ。parallel_forテンプレート関数は、次の図3のような処理を行う。
parallel_forテンプレート関数の引数は、反復処理の範囲を表すオブジェクト(「Range」クラスの派生オブジェクト)と、反復して行う処理を定義した関数オブジェクトの2つだ。parallel_forテンプレート関数は、与えられたRangeオブジェクトを適切に分割し、分割したオブジェクトを引数として関数オブジェクトを実行する、という処理を行う。TBBにはRangeクラスの派生クラスとして、閉区間(ループの開始点と終了点)を表す「blocked_range」や、2次元の範囲を表す「blocked_range2d」といった汎用的なオブジェクトがあらかじめ用意されているので、これらから適切なものを選択して使用すればよい。もちろん、独自のRange派生クラスを定義して使用することも可能だ。
たとえば、リスト2の処理をparallel_forテンプレート関数を用いて実装すると、次のリスト3のようになる。
リスト3 リスト2の処理をTBBで実装した例
#include "tbb/parallel_for.h"
#include "tbb/blocked_range.h"
class VectorAdder {
public:
double* vec1;
double* vec2;
VectorAdder( double* a, double* b ) :
vec1(a),
vec2(b)
{}
void operator() ( const tbb::blocked_rangeint r ) const {
for( int i = r.begin(); i != r.end(); i++ ) {
vec1[i] = vec1[i] + vec2[i];
}
}
};
void ParallelVectorAdd( double* a, double* b, int dim ) {
tbb::blocked_rangeint range = tbb::blocked_rangeint(0, dim, 100);
VectorAdder body = VectorAdder(a, b);
tbb::parallel_for( range, body );
}
それでは、このコードについて詳しく解説していこう。まず、parallel_forテンプレート関数は「parallel_for.h」で定義されており、利用の際はこのヘッダーファイルをincludeしておく必要がある。また、後述する「blocked_range」クラスも利用しているので、こちらが宣言された「blocked_range.h」も同様にincludeする。
#include "tbb/parallel_for.h" #include "tbb/blocked_range.h"
次に、実行したい処理を行う関数オブジェクトを用意する。ここでは配列同士の加算処理を行う「VectorAdder」クラスを作成している。処理に必要な変数等はクラスのメンバー関数として用意しておき、オブジェクトのコンストラクタで初期化を行う。
class VectorAdder {
public:
double* vec1;
double* vec2;
VectorAdder( double* a, double* b ) :
vec1(a),
vec2(b)
{}
parallel_forテンプレート関数で並列処理を実装する際に、キモとなるのがoperator()関数である。operator()関数はデータに対する処理を実装する関数で、引数には処理すべきデータ範囲を表すRange派生クラス型のオブジェクトを取る。ここではint型の閉区間を示すblocked_rangeintクラスを用いている。Rangeオブジェクトに対し、begin()関数を呼ぶことで反復処理すべき最初のデータを、end()関数を呼ぶことで最後のデータを取得できるので、この範囲でループを実行すればよい。
void operator() ( const tbb::blocked_rangeint r ) const {
for( int i = r.begin(); i != r.end(); i++ ) {
vec1[i] = vec1[i] + vec2[i];
}
}
};
以上のように関数オブジェクトを定義したら、あとはRangeオブジェクトと関数オブジェクトを作成し、それを引数にparallel_forテンプレート関数を呼び出せばよい。
void ParallelVectorAdd( double* a, double* b, int dim ) {
tbb::blocked_rangeint range = tbb::blocked_rangeint(0, dim, 100);
VectorAdder body = VectorAdder(a, b);
tbb::parallel_for( range, body );
}
まず、Rangeオブジェクトを作成する。関数オブジェクトの引数にはblocked_rangeint型のオブジェクトが渡されるので、ここでも同じ型のオブジェクトを作成する。なお、blocked_rangeのコンストラクタの引数は次のようになっている。
templatetypename Value class blocked_range {
:
:
blocked_range( Value begin, Value end, size_type grainsize=1 );
:
:
ここで「Value」はテンプレートで指定した型、「begin」は反復を開始する値、「end」は反復を終了する値である。また、「grainsize」はループを分割する際の粒度を指定する。通常は100~1000程度を指定すると良いようだ。
リスト3の例では、0から次元数(この例では変数「dim」に相当)-1までの整数に対して反復処理を行うため、引数にはそれぞれ「0」および「dim」を指定してblocked_rangeオブジェクトを作成し、VectorAdder型のオブジェクトとともにparallel_for関数に与えることで並列処理を実行している。
なお、上記で紹介したVectorAdd関数およびParallelVectorAdd関数を用いて、要素数が52428800(50×1024×1024)の配列の加算を行ったところ、その処理時間は次の表1のようになった。並列化を行うことで、大幅な高速化が行えていることが分かる。
| 関数 | 処理時間 |
|---|---|
| VectorAdd(非並列版) | 78ミリ秒 |
| ParallelVectorAdd(並列版) | 125ミリ秒 |
また、TBBにはループの並列化を行うアルゴリズムとしてparallel_forテンプレート関数のほか、並列プレフィックス演算を行うparallel_scanや、リダクション演算を行うparallel_reduceといったテンプレート関数も用意されている。これらについても、基本的にparallel_forテンプレート関数と同じような方法で利用が可能だ。