完了まで並列化: parallel_for_each

完了まで並列化: parallel_for_each#

いくつかのループでは、反復空間の最後が不明であるか、あるいはループ本体が、ループが終了する前に反復を追加することがあります。どちらの場合も、テンプレート・クラス oneapi::tbb::parallel_for_each を使用することで対処できます。

リンクリストは、終端が不明な反復空間の例です。並列プログラミングでは、リンクリストの項目へのアクセスは本質的にシリアルであるため、リンクリストの代わりに動的配列を使用するほうが効率的です。しかし、リンクリストを使用する必要があり、項目が安全に並列処理可能で、各項目の処理に少なくとも数千命令がかかる場合、parallel_for_each を使用して並列処理の恩恵を受けることができます。

例えば、次のシリアルコードについて考えてみます。

void SerialApplyFooToList( const std::list<Item>& list ) { 
    for( std::list<Item>::const_iterator i=list.begin() i!=list.end(); ++i ) 
        Foo(*i); 
}

Foo の実行に少なくとも数千命令が費やされる場合、parallel_for_each を使用するようにループを変換することで、並列処理をスピードアップできます。これには、const 修飾された operator() でオブジェクトを定義します。これは、operator()const である必要がある点を除いて、C++ 標準ヘッダー <functional> の C++ 関数オブジェクトに似ています。

class ApplyFoo { 
public: 
    void operator()( Item& item ) const { 
        Foo(item); 
    } 
};

SerialApplyFooToList の並列形式は、次のとおりです。

void ParallelApplyFooToList( const std::list<Item>& list ) { 
    parallel_for_each( list.begin(), list.end(), ApplyFoo() ); 
}

parallel_for_each を呼び出しても、2 つのスレッドが同時に入力イテレーターに対して動作することはありません。したがって、シーケンシャル・プログラムの入力イテレーターの一般的な定義は正しく機能します。この利便性により、ワークの取得がシリアルになるため、parallel_for_each はスケーラブルではなくなります。しかし、多くの場合、シーケンシャルに処理するよりスピードアップが期待できます。

parallel_for_each がスケーラブルにワークを取得する方法は 2 つあります。

  • イテレーターはランダム・アクセス・イテレーターにできます。

  • parallel_for_each のボディー引数は、parallel_for_each<Item>& タイプの 2 番目の引数フィーダーを取る場合、feeder.add(item) を呼び出すことでさらにワークを追加できます。例えば、ツリーのノードを処理することが、その子孫ノードを処理する前提条件であると仮定します。parallel_for_each を使用すると、ノードを処理した後、feeder.add を使用して子孫ノードを追加できます。Parallel_for_each のインスタンスは、すべての項目が処理されるまで終了しません。