CnCのチューニング
CnCではそれぞれのstepが処理単位です。CnCは使用可能なスレッドそれぞれにstepを割り振って処理を行います。上司が部下に仕事を割り振るがごとくスケジューリングが行われるのですが、このとき未処理の仕事があるにもかかわらず、仕事をせずに(できずに)ヒマを持て余している部下を極力減らすのが上司の務めです。場合によっては当初予定していた仕事の割り当てを変更(再スケジュール)しなければならないこともあるでしょう。とあるコックに飯を炊くが割り振られていたけれど、米を研ぐが完了していないがために待ち状態となりました。こんなとき未着手のタマネギを刻むに割り当てを変更してヒマそうなコックを減らします。
あるいは飯を炊くは米を研ぐに依存していることをあらかじめ教えておくことができるなら、少なくとも米を研ぐの前に飯を炊くを割り振らないようにスケジュールを組んで再スケジュールの手間を省くこともできるでしょうね。
全体のパフォーマンスを上げるため、tunerを仕込むことでCnCにさまざまなヒントを与えることができます。
stepのチューニング
step内でのお仕事は、itemから値をgetし/テキトーな処理を施して/得られた結果をitemにputすることです。itemから値をgetする際、その値が未確定(未だputされていない)であった場合はその値が確定する(putされる)までstepは待ち状態です。であれば、あらかじめ「このstepは特定のitemをgetする(=特定のitemに依存する)」ことを教えておけば、当該itemの値が確定するまで着火しない、つまりitemのgetで待ちに入ってヒマになるstepの着火が後回しになるようにスケジュールすることで、パフォーマンスの向上が期待できます。
フィボナッチ数のサンプルではn番stepはn-1番、n-2番itemをgetします。そのことをスケジューリングのヒントとして与えてやろうというわけ。
ではtunerを埋め込みましょう。CnC::step_tuner<>から導出したクラスを用意し、stepに差し込みます。
// stepに依存関係を教えてあげる tuner struct fib_tuner : public CnC::step_tuner<> { template<class dependency_consumer> void depends(const int& tag, fib_context &c, dependency_consumer& dC) const; }; // コンテキスト: tag/step/itemの集合およびそれらの依存関係を束ねる struct fib_context : public CnC::context< fib_context > { CnC::step_collection< fib_step, fib_tuner > m_steps; // ↑ココに差し込む … };
メンバ・テンプレート:depends()の定義はこんな。
template<class dependency_consumer> void fib_tuner::depends(const int& tag, fib_context &c, dependency_consumer& dC) const { if ( tag > 1) { // tag > 1 のとき、本stepは dC.depends(c.m_fibs, tag - 1); // c.m_fibsの tag-1番と tag-2番 に dC.depends(c.m_fibs, tag - 2); // 依存する! } }
stepのチューニングをもう一つ。前述のどのitemをgetするかを事前に教えておくことができないシチュエーションもあるでしょう。CnCはpre-scheduling(事前スケジューリング)の機能を提供しています。前準備(予行演習)として各stepに着火して値の確定していないitem-getによって待ちに入るかを調べ、待ちに入りそうなstepが後回しになるようスケジュールを組みます。値の確定していないitemが少ないとさほどに効かないとはいえ、多くの場合、より効率的なスケジューリングが可能です。
pre-schedulingを有効にするには、step_tunerのメンバとしてtrueを返すbool preschedule() constを定義するだけ。
// stepに依存関係を教えてあげる tuner struct fib_tuner : public CnC::step_tuner<> { bool preschedule() const { return true; } };
これと併せて、step内にflush_gets()を埋め込んでおくと、これ以降itemのgetは行われないことをCnCに教えてあげ、pre-scheduleを早々に切り上げさせて処理時間を短縮できます。
// フィボナッチ数列の第tag項を求める int fib_step::execute( const int & tag, fib_context & ctxt ) const { switch ( tag ) { case 0 : // fib(0) = 0 ctxt.m_fibs.put( tag, 0 ); break; case 1 : // fib(1) = 1 ctxt.m_fibs.put( tag, 1 ); break; default : // fib(n) = fib(n-1) + fib(n-2) // 前の二つを手に入れて fib_type f_1; ctxt.m_fibs.get( tag - 1, f_1 ); fib_type f_2; ctxt.m_fibs.get( tag - 2, f_2 ); ctxt.flush_gets(); // pre-schedulingはここまでやれば十分だよ!! // 加えたものが結果となる ctxt.m_fibs.put( tag, f_1 + f_2 ); } return CnC::CNC_Success; }
itemのチューニング
ヨソから入れ知恵をしてもらえない限り、CnCは各stepが一定数以上のitem-getを行わないことを知るすべがありません。各stepにあてがわれたitem-get用内部バッファからitem-dataを削除できないってことです。CnCのプログラミング・モデルでは、物事がいつ起こるかを宣言しないので、最後にgetされるitemがどれであるかを知ることもできません。かといってデータをいつまでもメモリ(バッファ)内に保持していたら、いつしかメモリを食い潰しかねないので、無駄なデータの保持は極力避けたいところです。
CnCに対してstep内で行われるitem-get数:get-countを教えてあげることができます。CnCはstep内でgetされたitem数がget-countに達したとき、内部バッファから余分な領域を解放します。例えばフィボナッチ数を求めるfib_stepではgetされるitemは最大2ですから、get-countを2とすることができます。
get-countはitemに関する属性ですから、get-countの設定はitem_tunerで行います。
// itemのための内部バッファを節約する struct item_tuner : public CnC::hashmap_tuner { int get_count(const int& tag) const { return tag > 1 ? 2 : 1; } }; // コンテキスト: tag/step/itemの集合およびそれらの依存関係を束ねる struct fib_context : public CnC::context< fib_context > { CnC::step_collection< fib_step, fib_tuner > m_steps; CnC::item_collection< int, fib_type, item_tuner > m_fibs; // ↑ココに差し込む …
tagのチューニング
実行されるstepが状態を持たない(stateless)であれば、何度着火されようが入力(item)が変化しない(immutable)のだから、同一の結果が得られるでしょう。ならば何度も着火するのは無駄なオーバヘッドを増やすだけ。デフォルトではCnCが同一tagを何度もputすると、それと同じ回数だけ対応するstepに着火します。が、tagにtunerを埋め込んで無駄な着火を抑えることができます。tagの重複は一般的ではない(通常やらない)し、重複の抑止を自動的に行うこと自体がそれなりのオーバヘッドとなるので、デフォルトにはなっていません。
重複tagを抑止するには、tag_collectionにtunerを埋め込みます。
// コンテキスト: tag/step/itemの集合およびそれらの依存関係を束ねる struct fib_context : public CnC::context< fib_context > { CnC::step_collection< fib_step, fib_tuner > m_steps; CnC::item_collection< int, fib_type, item_tuner > m_fibs; CnC::tag_collection< int, CnC::preserve_tuner<int> > m_tags; // ↑ココに差し込む …
以上、CnCのデバッグとチューニングに関するトピックをざっくりと紹介しました。どちらにせよ、処理(step)本体には直接手を触れず(flush_gets()だけは例外)、外付けで調整できるのはナイスなからくりです。
コンテキストの生成に先立ってCnC::debug::set_num_threads(int nthr);を呼ぶことで、CnCが使うスレッド数を調整できるみたい。スケーラビリティの確認や、コアの少ないマシンでちゃんと動くかの確認に使えそうです。