SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

インテルTBBによる選択ソートの高速化

スレッドを意識しないマルチスレッドアプリケーション


  • X ポスト
  • このエントリーをはてなブックマークに追加

再帰的なタスク実行

 ソート対象を前半と後半の2つに分割し、それぞれをソートすることで高速化できました。ならば前半/後半をソートする際、それぞれをさらに2分割すればもっと速くなるでしょうね。これを繰り返せば少ない要素群をソートする多数のタスクが作られることになりますが、論理スレッド数が増えるわけではないのでスレッドの切り替えに要するオーバヘッドは気にしなくて済みそうです。parallel_invokeを再帰的に使いましょう。

class extremely_sort : public sort_base {
public:
    typedef std::vector<std::string>::difference_type difference_type;
  extremely_sort(iterator first, iterator last) : sort_base(first,last) {}
protected:
  static difference_type cutoff_;
  virtual void do_sort() const {
      difference_type size = std::distance(first_,last_);
    // そこそこ小さくなったら選択ソートに切り替え
    if ( size < cutoff_ ) {
      selection_sort(first_, last_);
    } else {
      iterator mid = first_;
      std::advance(mid, size/2);
      nth_element(first_, mid, last_);
      tbb::parallel_invoke(extremely_sort(first_,mid), extremely_sort(mid,last_));
    }
  }
};

extremely_sort::difference_type  extremely_sort::cutoff_ = 30;

 筆者の環境でのここまでの各ソートの処理時間は以下のようになりました。

normal_sort ....... 26.0291 [sec]
ultra_sort ........ 13.303 [sec]
super_sort ........ 6.77076 [sec]
extremely_sort .... 0.0596765 [sec]

 再帰版 extremely_sort のスピードは normal_sort の実に436倍。2-coreでの測定結果ですから、4-core/8-core機であればさらに良い結果が得られることでしょう。

 ……実は2分割を再帰的に行った時点で選択ソートがクイックソートになっちゃってます。クイックソートの時間計算量は N・logN なので速いのはアッタリマエなのです。

再帰を使わない実装

 インテルTBBでのプログラミングに慣れるべくチュートリアルドキュメントを読んでいて、parallel_do という一風変わった並列アルゴリズムを見つけました。さきほどの再帰parallel_invokeとは異なる実装を紹介しましょう。

 parallel_invokeは複数の関数オブジェクトに同時に着火してそれらの完了を待つのに対し、parallel_doで着火される関数オブジェクトは1つだけ。1つの関数オブジェクトを(引数を取換えながら)何度も呼び出します。

 parallel_doに与える引数は処理に与える要素列を示すための2つ(先頭と末尾)一組のイテレータ、そしてそれらイテレータが指す要素を引数として処理を行う関数オブジェクトです。

 簡単な例を考えました。kitchen(中華料理屋の厨房)です。kitchenはお客から受けた注文に従って調理を行います。注文伝票の中には「○○定食」がいくつか混じっています。kitchenではたとえば「ギョーザ定食」という注文に対してはギョーザの調理にあわせて「白飯」と「スープ」の注文伝票を自分自身に追加発行することで「○○定食」を処理します。「○○定食」の処理 = ○○の調理 + 「白飯」と「スープ」の追加注文 ってことです。

#include <iostream>
#include <string>
#include <vector>

#include <tbb/tbb.h>

using namespace std;

// とある中華料理屋の厨房(キッチン)
class kitchen {
  mutable tbb::mutex mutex_;
public:
  void operator()(const string& order, tbb::parallel_do_feeder<string>& feeder) const {
    string dish;
    string::size_type pos = order.find("定食");
    if ( pos != string::npos ) {
      // ××定食だったら"白飯"と"スープ"を注文に加える
      dish = order.substr(0,pos);
      feeder.add("白飯(" + dish + ")");
      feeder.add("スープ(" + dish + ")");
    } else {
      // 単品だったら調理開始
      dish = order;
    }
    // 出力が乱れないようlockする
    tbb::mutex::scoped_lock lock(mutex_);
    cout << dish << endl;
  }
};

int main() {
  // オバちゃんが注文を取る
  vector<string> orders;
  orders.push_back("ラーメン");
  orders.push_back("ギョーザ定食");
  orders.push_back("ビール");
  orders.push_back("レバニラ炒め定食");

  // parallel_doを用い、厨房が注文をさばく
  tbb::parallel_do( orders.begin(), orders.end(), kitchen());
}
実行結果(※ 出力順は実行環境によって異なります)
レバニラ炒め
ビール
スープ(レバニラ炒め)
ギョーザ
白飯(レバニラ炒め)
スープ(ギョーザ)
ラーメン
白飯(ギョーザ)

 関数オブジェクトのoperator()の第2引数 parallel_do_feeder がキモ。こいつに新たな注文をaddすることでkitchen自らに対し追加注文できるわけです。

 具体的にソートに適用したサンプルとして、記事添付ファイルに「selection_sort.cpp」を収録したので、実際に確めてみてください。

まとめ

 インテルTBB、文句なく面白いです。Win32-threadやpthreadなどとは異なり、スレッドを意識せずにスケーラブルなマルチスレッド・アプリケーションが書けるのが魅力的です。インテルTBBによるマルチスレッドで遊んでみませんか?

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/5039 2012/07/09 15:32

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング