インテル® C++ コンパイラー 18.0 デベロッパー・ガイドおよびリファレンス

ホルダーの使用例

インテル® Cilk™ Plus は古い機能 (非推奨) です。代わりに、OpenMP* またはインテル® TBB を使用してください。詳細は、「インテル® Cilk™ Plus の代わりに OpenMP* またはインテル® TBB を使用するためのアプリケーションの移行」を参照してください。

この例は、ホルダーの使用方法を示します。

compute() 関数は、メモ化アルゴリズムを使用して値を計算し、中間結果をハッシュテーブルに格納する複合関数です。compute() はほかの関数を呼び出し、それらの関数もほかの関数を呼び出します (すべての関数はグローバル・ハッシュ・テーブルを共有します)。関数は全部で 12 以上あり、ハッシュテーブルの参照は合計で約 60 になります。

hash_table<int, X> memos;
void h(const X& x);  // memos を使用
double compute(const X& x)
{
    memos.clear();
    // ...
    memos[i] = x;
    ...
    g(i); // memos を使用
    // ...
    std::for_each(c.begin(), c.end(), h); // c の各要素に対して h を呼び出す
 }
 int main()
 {
    const std::size_t ARRAY_SIZE = 1000000;
    extern X myArray[ARRAY_SIZE];
    for (std::size_t i = 0; i < ARRAY_SIZE;++i)
    {
        compute(myArray[i]);
 	  }
 }

上記の例でインテル® Cilk™ Plus を使用するには、mainfor ループを cilk_for に置き換えます。

ハッシュテーブルは compute() の各呼び出しのエントリーでクリアされ、ハッシュテーブルに格納された値は compute() がリターンした後に使用されませんが、ハッシュテーブルをグローバル変数として使用しているため、compute() を並列で安全に呼び出すことはできません。この状況を改善する 1 つの方法は、memoscilk_for ループ内のプライベート変数にして、計算時に渡すことです。つまり、各ループ反復ごとにプライベート・コピーを持つことになります。

cilk_for (std::size_t i = 0; i < ARRAY_SIZE; ++i)
{
    hash_table<int,X> memos;
    compute(myArray[i], memos);
}

この方法では、compute、h、g 関数と memos を参照するすべての関数、およびこれらの関数を呼び出すすべての関数宣言を変更する必要があります。この compute とほかの関数への変更により、オリジナルの抽象インターフェイスの一部ではなかった実装の情報が必要になります。さらに、関数 h は固定のインターフェイスが必要なテンプレート・アルゴリズム for_each で呼び出されます。そして、ループを通るたびに毎回 hash_table のコンストラクターとデストラクターのオーバーヘッドが発生します。

代わりの方法は、memos をホルダーに置き換えることです。ホルダーは含まれているすべての関数で利用可能ですが、並列ループの反復で競合が発生しません。このためには、memos 変数を使用している部分をホルダーを使用するように変更する必要があります。

cilk::holder<hash_table<int, X> > memos_h;
void h(const X x); // memos_h を使用
double compute(const X& x)
{
    *memos_h->clear(); // ホルダーを逆参照してコンテンツにアクセス
    // ...
    (*memos_h)[i] = x; // ホルダーを逆参照してコンテンツにアクセス
    ...
    g(i); // memos_h を使用
    // ...
    std::for_each(c.begin(), c.end(), h); // c の各要素に対して h を呼び出す
 }

レデューサーと同様に、ホルダーが保持する値にアクセスするには、ホルダーを "逆参照" する必要があります。多くの変更が必要になる場合は、hash_table と同じインターフェイスを持ち、ホルダーへの呼び出しをすべてリダイレクトするクラスでホルダーをラップすることができます。

template <typename K, typename V>
class hash_table_holder
{
private:
    cilk::holder<hash_table<K, V> > m_holder;
public:
    void clear() { m_holder()->clear(); }
    V& operator[](const K& x) { return (*m_holder)[x]; }
    std::size_t size() const { return m_holder->size(); }
    // etc. ...
 };

上記のラッパーを使用すると、hash_tablehash_table_holder に、forcilk_for に置き換えることを除いて、オリジナルのコードを変更する必要はありません。

hash_table_holder<int, X> memos;
void h(const X& x); // memos を使用
double compute(const X& x)
 {
    memos.clear(); // hash_table_holder::clear() の呼び出し
    // ...
 }

上記で示されているすべての変更において、スレッド・ローカル・ストレージを使用する利点はありません (移植性のためを除く)。ただし、関数の 1 つが cilk_spawn を使用する場合を考慮してください。

void h(const X& x)
{
    Y y = x.nested();
    double d, w;
    if (y)
    {
       w = cilk_spawn compute_width(y); // 'memos' を使用可能
       d = compute_depth(y); // 'memos' は使用不可
       cilk_sync;
       compute(y); // 再帰呼び出し。'memos' を使用
    }
 }

上記の例では、compute_width 内のホルダーのビューは h のエントリーのビューと同じです。より重要なのは、異なるワーカーが再帰呼び出しを実行している場合でも、compute への再帰呼び出しにおいてホルダーのビューは h のエントリーのビューと同じであることです。このため、インテル® Cilk™ Plus プログラム内のホルダービューには、スレッド・ローカル・ストレージでは得られない利点があります。