yohhoyの日記

技術的メモをしていきたい日記

Intel TBB 4.0のC++11対応状況

Intel TBB(Threading Building Blocks) 4.0では、C++11で追加された新機能との親和性向上や、新しいC++標準ライブラリ互換APIの提供が行われている。

C++11標準ライブラリ互換APIの提供

TBB 4.0ではC++11標準ライブラリとの前方互換性のために、新しく追加されたC++標準ライブラリ互換APIをいくつか提供する。TBB提供のC++11互換ヘッダをincludeすると、それぞれ名前空間 std に下記の識別子が導入される。

  • ヘッダ tbb/compat/condition_variable
    • defer_lock_t, try_to_lock_t, adopt_lock_t 構造体
    • defer_lock, try_to_lock, adopt_lock 定数
    • lock_guard, unique_lock クラステンプレート
    • swap 特殊化関数テンプレート*1
    • condition_variable クラス
    • cv_status 列挙型 と timeout, no_timeout 列挙子
  • ヘッダ tbb/compat/thread
    • thread クラス
    • this_thread 名前空間(get_id, yield, sleep_for 関数)

C++11標準ライブラリ std::condition_variable と、TBB 4.0が提供する互換APIとの差異は次の通り。

  • 内部実装において標準 std::chrono の代わりに、tbb::tick_count を利用している。
  • 内部実装において標準 std::mutex の代わりに、tbb::mutex を利用している。
  • 内部実装において std::system_error の代わりに、std::runtime_error を利用している。(system_errorがサポートされれば移行予定)
  • インタフェース宣言においてnoexceptやexplicit operatorなどのC++11追加構文は利用しない。
  • condition_variable_anyクラス, notify_all_at_thread_exit関数*2は提供しない。

C++11標準ライブラリ std::thread と、TBB 4.0が提供する互換APIとの差異は次の通り。

C++11 TBB 4.0
std::this_thread::sleep_for()関数テンプレートはchrono::duration型を引数にとる std::this_thread::sleep_for()関数はtbb::tick_count::interval_t型を引数にとる
ムーブ・セマンティクス/rvalue referenceを利用 利用しない
std::threadコンストラクタは任意個の引数をとることができる コンストラクタは0〜3個の引数のみとることができる
swap()メンバ関数を持つ なし

ラムダ式との親和性

下記のTBB機能とともにラムダ式を用いると、ファンクタを別途記述する必要がなくなりソースコード記述を簡略化できる。特に parallel_pipeline, structured_task_group, flow::graph はラムダ式との親和性が高い設計となっている。(もちろんファンクタと組み合わせることも可能)

  • 各種並列アルゴリズムに渡す関数オブジェクト
  • parallel_pipelineアルゴリズムに渡すフィルタ作成(make_filter関数テンプレート)
  • structured_task_group, task_groupに渡すタスク本体処理(make_task関数テンプレート)
  • flow::graphに渡すノード本体処理(continue_node, function_node, source_node, sequencer_node クラス)

また、ファンクタ用いる場合はソースコード上の本体処理の定義と利用箇所が離れがちだが、ラムダ式では本体処理定義と利用箇所が局所化されるため、ソースコードの可読性向上が期待される。

#include <array>
#include "tbb/parallel_for.h"
const int N = /*...*/;

//-------- ファンクタ版
struct ParallelBody {
  typedef std::array<int, N> ArrayType;
  ArrayType& arr_;
  ParallelBody(ArrayType& arr) : arr_(arr) {}
  void operator()(int i) const {
    arr_[i] *= 2;
  }
};
//...
std::array<int, N> arr1 = /*...*/;
tbb::parallel_for(0, N, ParallelBody(arr1));

//-------- ラムダ式版
std::array<int, N> arr2 = /*...*/;
tbb::parallel_for(0, N, [&](int i){
  arr2[i] *= 2;
});

std::exception_ptr型と例外伝搬

各種並列アルゴリズムでは暗黙的にワーカースレッドを利用するため、アルゴリズム呼び出し元と処理本体は異なるスレッド上で実行される。このため、ユーザ定義の処理本体で例外throwが行われた場合に、呼び出し元までスレッドをまたいで例外伝搬を行う仕組みが必要となる。
コンパイラC++11で追加されたstd::exception_ptr型をサポートし、かつマクロ TBB_USE_CAPTURED_EXCEPTION=0*3 の場合は、スレッドをまたぐ例外伝搬にstd::exception_ptrが利用される。C++11のstd::exception_ptrを利用することで、例外を正確に(=オリジナルの型のまま)伝搬できるようになる。*4

#include "tbb/parallel_invoke.h"
//-------- std::exception_ptrを利用した例外伝搬
try {
  tbb::parallel_invoke(
    [](){ /*...*/ };
    [](){ throw int(42); };  // (1) int型の例外throw
  );
} catch (int value) {
  assert(value == 42);       // (2) int型として例外catch
}

C++11非対応コンパイラの場合は、次のルールに従って例外伝搬のエミュレーションが行われる。ただし例外オブジェクトがtbb::tbb_exception型またはその派生クラスでない場合、tbb::captured_exceptionで置き換えられてしまいオリジナル例外の型情報は失われることに注意。

  1. 例外オブジェクトeがtbb::tbb_exceptionの場合、e.move() メンバ関数を用いてオリジナルの例外オブジェクトが伝搬される。
  2. 例外オブジェクトeがstd::exceptionの場合、TBBランタイムが新たにthrowした例外 tbb::captured_exception(typeid(e).name(),e.what()) として伝搬される。
  3. いずれでもない場合、例外 tbb::captured_exception として伝搬される。TBBランタイムが新たにthrowするcaptured_exceptionのname(), what()は実装依存となる。
#include "tbb/parallel_invoke.h"
//-------- C++11非対応コンパイラでの例外伝搬エミュレーション
try {
  tbb::parallel_invoke(
    [](){ /*...*/ };
    [](){ throw int(42); };  // (1) int型の例外throw
  );
} catch (int value) {
  // (2) int型では例外catchできない
} catch (tbb::captured_exception& e) {
  // (3) tbb::captured_exception型として例外catch
  //     元の int型 値42 という情報は失われてしまう(実装依存)
}

<非対応> ムーブ・セマンティクス/rvalue reference

TBB 4.0ではムーブ・セマンティクス/rvalue referenceを利用しない。並列コンテナへの値挿入や、並列アルゴリズムに渡す関数オブジェクトなどではconst lvalue referenceまたは値コピーが行われる。

<非対応> 可変引数テンプレート(Variadic Templates)

TBB 4.0では可変引数テンプレートを利用しない。parallel_invokeアルゴリズムでは可変引数テンプレートを用いず、テンプレート引数の個数毎に最大10引数までオーバーロード関数を用意している。

*1:unique_lockに対して特殊化されたswap関数

*2:TBB Reference ManualによればC++11(当時はC++0X)についてはN3000をベースにしたとあり、N3070 - Handling Detached Threads and thread_local Variablesで追加提案された同関数は含まれない。

*3:TBB 4.0では、MSVC10以降またはgcc4.4以降のとき既定で TBB_USE_CAPTURED_EXCEPTION=0 が設定される。

*4:C++11より前の標準C++98/03では"スレッド"を定義しないため、単一スレッドのプログラムの意味論しか定義していなかった。このためC++98/03の範囲内では、スレッドをまたぐ例外伝搬をポータブルに行なう方法は存在しない。