CodeZine(コードジン)

特集ページ一覧

インテルTBBから学ぶループの並列化

インテルTBBを通じて学ぶ並列処理(2)

  • LINEで送る
  • このエントリーをはてなブックマークに追加
2009/11/11 14:00
目次

インテル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のコードを読んで実行して見て下さい。そうすれば、関数呼び出し演算子の定義方法と大まかな動作が分かると思います。

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では関数呼び出し演算子を多重定義して、直列のものとほぼ同じ計算プログラムをコーディングしているだけです。比較して見ると定義方法以外はプログラムがほぼ同じです。

直列計算プログラムと並列計算プログラムをSalesオブジェクトから抜粋
//直列処理
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)のインスタンスを指定するだけです。

parallel_forテンプレート呼び出し部分をAnalyzerオブジェクトより抜粋
/* 
    売上明細で算出するべき項目を【並列で】計算します。
*/
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を使用する際には、何度か実行して処理効率を確認して下さい。

 次項では少し複雑な並列化処理を採り上げます。


  • LINEで送る
  • このエントリーをはてなブックマークに追加

あなたにオススメ

著者プロフィール

  • インドリ(インドリ)

    分析・設計・実装なんでもありのフリーエンジニア。 ブログ「無差別に技術をついばむ鳥(http://indori.blog32.fc2.com/)」の作者です。 アドバイザーをしたり、システム開発したり、情報処理技術を研究したりと色々しています。 座右の銘は温故知新で、新旧関係なく必要だと考えた...

バックナンバー

連載:インテルTBBを通じて学ぶ並列処理
All contents copyright © 2005-2021 Shoeisha Co., Ltd. All rights reserved. ver.1.5