粒子法シミュレーションに本格的にハマっていくうちに、計算時間が気になりだしたので、せっかくCore2Duoのマシンなんだし(Quadでないのが悔やまれた!)、マルチスレッド化でもしてみようかと、インテルのTBBライブラリの、parallel_for機能を使ってみた。粒子法では、同時刻における個々の粒子の挙動は全て互いに独立しているので、並列化にはもってこいの題材でもある。それに、TBBは今やオープンソースだ。
ここから誰でもダウンロード取得できる。
並列化というと何やら面倒そうに聞こえるが、TBBにおいては、その考え方はstlの
for_eachを呼ぶのとほとんど同じで、コーディングする上でそれ以上のことを考える必要は一切ない。要はfor_eachのTBB版を呼べば、それが自動的にマルチスレッドで並列して走ってくれるのだ。
parallel_forは簡単に、stl::for_each のTBB版と考えることができる。stl::for_eachと同じく関数テンプレートとして実装されており、ループをまわしたい範囲の開始点と終了点と、各要素について何を呼び出すかを指定するためのFunction述語を受け取る点も、考え方としては全く同じだ。
まずはよく使い慣れた、for_eachのインタフェースがこれ。
// stl::for_each template< class IterType, class Function > Function for_each( IterType first, IterType last, Function func ){...}
そして、以下が、parallel_forのインタフェースだ。
// tbb::parallel_for template<typename Range, typename Body> void parallel_for( const Range& range, const Body& body ) {...}
stl::for_eachと大きく違う点は、stl::for_eachにおける一番最後のFunctionの引数が、ループで回る要素そのものを一つ受け取るUnaryFunctionではなく、Rangeという、要素のコンテナ的な型を受け取るようになっている点だ。
(これを見ればわかるように、for_each におけるFunction にあたるものは、
parallel_for では、Body という型名になっている。)
parallel_for のテンプレート引数型としての、BodyクラスやRangeクラスが満たさなければならない、コンセプトは、TBB conceptsに明記されている。(これは一式をダウンロードしたときについてくるドキュメントの中でも確認できる。)
しかし、心配は無用だ。
Rangeの標準実装としてのblocked_range
それでは実際の使い方を見ていくことにしよう。
VC9にて空のプロジェクトを作成し、以下をビルド実行すれば、ネイティブループと、TBBを使った場合とのパフォーマンスの違いが体感できるはずだ。
(勿論tbbヘッダ一式への追加のインクルードパス設定やtbb.libへのリンクパスの設定、実行時のtbb.dll へのパスが通っていることが必要だ。)
#include <iostream> #include <time.h> #include <vector> #include <cmath> using namespace std; //TBB関連組み込みファイル #include "tbb/task_scheduler_init.h" #include "tbb/blocked_range.h" #include "tbb/parallel_for.h" #ifdef _DEBUG #pragma comment( lib, "tbb_debug.lib" ) #else #pragma comment( lib, "tbb.lib" ) #endif class Node { public: Node( vector< float >* results ){ m_results = results; } ~Node( void ){} void operator()( const tbb::blocked_range< int >& range ) const{ for( int i=range.begin(); i!=range.end(); ++i ){ m_results->operator[](i) = pow( (double)i, 5 ); }// i } private: vector< float >* m_results; }; int main( int artc, char* argv[] ) { const int num = 1.0e8; vector< float > results; { results.resize( num ); } // Native Solution clock_t time0 = clock(); { for( int i=0; i<num; ++i ){ results[i] = pow( (double)i, 5 ); }// i } clock_t time1 = clock(); // // TBB Solution clock_t time2 = clock(); { tbb::task_scheduler_init tbb; // parallel_for呼び出し tbb::parallel_for( tbb::blocked_range<int>( 0, num, 1000 ), Node( &results ) ); // ↑0番目からnum番目までの範囲を、1000要素単位で重複なく区切り、 // その区切られた範囲のそれぞれを各スレッドにて、 // Node型関数オブジェクトのoperator() の引数として呼ばせる。 tbb.terminate(); } clock_t time3 = clock(); // cout << "native solution takes " << (time1-time0)/(float)CLOCKS_PER_SEC <<" sec"<<endl; cout << "TBB solution takes " << (time3-time2)/(float)CLOCKS_PER_SEC <<" sec"<<endl; return 0; }
以上のサンプルでは1000要素を区切り単位としているが、ループの中の処理によって、どのような値を区切り単位にするかで、パフォーマンスは結構違ってくるようだ。あまり細かく区切りすぎてもいけないようだ。
ちなみに筆者の環境では、コアが2個しかないのだが、区切り数1000で、パフォーマンスは大体2倍になった。
インテル スレッディング・ビルディング・ブロック ―マルチコア時代のC++並列プログラミング
- 作者: James Reinders,菅原清文,エクセルソフト株式会社
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2008/02/23
- メディア: 単行本
- 購入: 3人 クリック: 95回
- この商品を含むブログ (27件) を見る