オープンソース化された並列化テンプレートクラスライブラリ「Intel Threading Building Blocks」入門 3ページ

要素数が分かっているデータ群に対する反復処理の並列実装(forループの並列化)

 並列処理が有効な典型的な例として、配列や各種コンテナに格納されたデータに対し、始点と終点を指定して次々と反復処理を行う、というパターンがある。リスト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のような処理を行う。

図5 parallel_forテンプレート関数の動作
図5 parallel_forテンプレート関数の動作

 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_range<int>& 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_range<int> range = tbb::blocked_range<int>(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_range<int>クラスを用いている。Rangeオブジェクトに対し、begin()関数を呼ぶことで反復処理すべき最初のデータを、end()関数を呼ぶことで最後のデータを取得できるので、この範囲でループを実行すればよい。

    void operator() ( const tbb::blocked_range<int>& 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_range<int> range = tbb::blocked_range<int>(0, dim, 100);
    VectorAdder body = VectorAdder(a, b);
    tbb::parallel_for( range, body );
}

 まず、Rangeオブジェクトを作成する。関数オブジェクトの引数にはblocked_range<int>型のオブジェクトが渡されるので、ここでも同じ型のオブジェクトを作成する。なお、blocked_rangeのコンストラクタの引数は次のようになっている。

template <typename 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のようになった。並列化を行うことで、大幅な高速化が行えていることが分かる。

表1 VectorAddおよびその並列版であるParallelVectorAddの実行パフォーマンス
関数 処理時間
VectorAdd(非並列版) 78ミリ秒
ParallelVectorAdd(並列版) 125ミリ秒

 また、TBBにはループの並列化を行うアルゴリズムとしてparallel_forテンプレート関数のほか、並列プレフィックス演算を行うparallel_scanや、リダクション演算を行うparallel_reduceといったテンプレート関数も用意されている。これらについても、基本的にparallel_forテンプレート関数と同じような方法で利用が可能だ。