C/CPPでの作業盗用キューの適切な実装を探しています。私はグーグルを見回しましたが、役に立つものは何も見つかりませんでした。
おそらく誰かが優れたオープンソースの実装に精通しているでしょうか? (私は元の学術論文から取られた擬似コードを実装したくない)。
無料の昼食はありません。
ご覧ください オリジナル作品盗み紙 。この論文は理解しにくいです。紙には擬似コードではなく理論的な証明が含まれていることを私は知っています。ただし、TBBほどはるかに単純なバージョンはありません。もしあれば、それは最適なパフォーマンスを提供しません。作業自体を盗むにはある程度のオーバーヘッドが発生するため、最適化とトリックは非常に重要です。特に、デキューはスレッドセーフである必要があります。高度にスケーラブルでオーバーヘッドの少ない同期を実装することは困難です。
なぜあなたがそれを必要とするのか本当に疑問に思っています。 適切な実装はTBBやCilkのようなものを意味すると思います。繰り返しますが、仕事を盗むことは実装するのが難しいです。
Intelのスレッディングビルディングブロックをご覧ください。
「ワークスティーリング」を実装することは、理論的には難しいことではありません。より多くの作業を行うには、コンピューティングと他のタスクの生成を組み合わせて機能するタスクを含むキューのセットが必要です。また、新しく生成されたタスクをそれらのキューに配置するには、キューへのアトミックアクセスが必要です。最後に、タスクを実行したスレッドの作業をさらに見つけるために、各タスクが最後に呼び出すプロシージャが必要です。そのプロシージャは、作業を見つけるために作業キューを調べる必要があります。
このようなワークスティーリングシステムのほとんどは、スレッドの数が少なく(通常は実際のプロセッサコアによってバックアップされます)、スレッドごとに1つのワークキューがあることを前提としています。次に、最初に自分のキューから作業を盗もうとします。空の場合は、他のキューから盗もうとします。トリッキーになるのは、どのキューを調べるかを知ることです。作業のためにそれらを連続してスキャンすることはかなり費用がかかり、作業を探しているスレッド間で大量の競合を引き起こす可能性があります。
これまでのところ、これは1つの2つの主要な例外を除いて、すべてかなり一般的なものです。1)コンテキストの切り替え(たとえば、「スタック」などのプロセッサコンテキストレジスタの設定)は、純粋なCまたはC++では記述できません。これは、パッケージの一部をターゲットプラットフォーム固有のマシンコードで記述することに同意することで解決できます。 2)マルチプロセッサのキューへのアトミックアクセスは、純粋にCまたはC++では実行できないため(デッカーのアルゴリズムを無視)、X86 LOCKXCHやCompareandSwapなどのアセンブリ言語同期プリミティブを使用してコーディングする必要があります。これで、安全にアクセスできるようになったらキューを更新するためのコードはそれほど複雑ではなく、Cの数行で簡単に記述できます。
ただし、このようなパッケージをCおよびC++で混合アセンブラーを使用してコーディングしようとすると、まだかなり非効率的であり、最終的にはアセンブラーですべてをコーディングすることになります。残っているのはC/C++互換のエントリポイントです:-}
私はこれを [〜#〜] parlanse [〜#〜] 並列プログラミング言語のために行いました。これは、任意の数の並列計算がライブで、いつでも相互作用(同期)するというアイデアを提供します。これは、CPUごとに1つのスレッドを使用してX86のバックグラウンドで実装され、実装は完全にアセンブラーで行われます。作業を盗むコードはおそらく合計1000行であり、競合のない場合に非常に高速にする必要があるため、そのトリッキーなコードです。
CおよびC++の軟膏の本当の目的は、作業を表すタスクを作成するときに、どのくらいのスタックスペースを割り当てるかということです。シリアルC/C++プログラムは、膨大な量(たとえば、10Mb)のone線形スタックを単純に全体的に割り当てることでこの質問を回避し、誰もその量をあまり気にしませんスタックスペースが無駄になります。しかし、何千ものタスクを作成し、それらすべてを特定の瞬間に実行できる場合、それぞれに10Mbを合理的に割り当てることはできません。したがって、タスクに必要なスタックスペースの量を静的に決定する必要があるか(Turing-hard)、または広く利用可能なC/C++コンパイラでは実行されないスタックチャンクを割り当てる必要があります(関数呼び出しごとなど)。 (たとえば、使用している可能性が高いもの)。最後の方法は、タスクの作成を抑制して、いつでも数百に制限し、ライブのタスク間で数百の非常に巨大なスタックを多重化することです。タスクが状態をインターロック/一時停止できる場合は、しきい値に達するため、最後の作業を行うことはできません。したがって、これを実行できるのは、タスクonlyが計算を実行する場合のみです。それはかなり厳しい制約のようです。
PARLANSEの場合、関数呼び出しごとにヒープにアクティベーションレコードを割り当てるコンパイラを構築しました。
Pthreadまたはboost :: thread上に構築されたC++でのスタンドアロンのワークスティーリングキュークラスの実装を探しているなら、幸運を祈ります。私の知る限り、それはありません。
ただし、他の人が言っているように、Cilk、TBB、およびMicrosoftのPPLはすべて、内部でワークスティーリングの実装があります。
問題は、ワークスティーリングキューを使用するのか、それとも実装するのかということです。 1つだけを使用したい場合は、上記の選択肢が適切な開始点であり、いずれかで「タスク」をスケジュールするだけで十分です。
BlueRajaがPPLのtask_group&structured_task_groupがこれを行うと述べたように、これらのクラスはIntelのTBBの最新バージョンでも利用可能であることに注意してください。並列ループ(parallel_for、parallel_for_each)も実装されていますワークスティーリング付き。
実装を使用するのではなくソースを調べる必要がある場合、TBBはオープンソースであり、MicrosoftはCRTのソースを出荷しているため、調査を行うことができます。
Joe DuffyのブログでC#の実装を確認することもできます(ただし、C#であり、メモリモデルは異なります)。
-リック
非常にエレガントな方法でそれを簡単に行うためのツールがあります。これは、非常に短時間でプログラムを並列化するための非常に効果的な方法です。
HPCチャレンジアワード
HPCチャレンジクラス2アワードのCilkエントリーは、2006年の「エレガンスとパフォーマンスのベストコンビネーション」賞を受賞しました。この賞は、2006年11月14日にタンパで開催されたSC'06で行われました。
OpenMPは、再帰的並列処理と呼ばれていますが、ワークスティーリングを非常によくサポートしている可能性があります。
OpenMP仕様では、タスク構造(ネストできるため、再帰的な並列処理に非常に適しています)を定義していますが、それらの実装方法の詳細は指定していません。 gccを含むOpenMP実装は、通常、タスクを盗む何らかの形式の作業を使用しますが、正確なアルゴリズム(および結果のパフォーマンス)は異なる場合があります。
見る #pragma omp task
および#pragma omp taskwait
更新
本の第9章 C++ Concurrency in Action は、「プールスレッドのワークスティーリング」を実装する方法を説明しています。私はそれを自分で読んだり実装したりしていませんが、それほど難しくはありません。
structured_task_group[〜#〜] ppl [〜#〜] のクラスは、実装にワークスティーリングキューを使用します。スレッド化にWSQが必要な場合は、それをお勧めします。
実際にソースを探しているのなら、コードがppl.hで提供されているのか、プリコンパイルされたオブジェクトがあるのかわかりません。今夜家に帰ったらチェックしなければなりません。
このオープンソースライブラリ https://github.com/cpp-taskflow/cpp-taskflow 2018年12月以降、スレッドプールを盗む作業をサポートしています。
論文「DynamicCircularWork-stealing Deque」、SPAA、2015で説明されているように、ワークスティーリングキューを実装するWorkStealingQueue
クラスを見てください。
このCプロジェクト をC++に移植しました。
元のSteal
は、配列が展開されるときにダーティ読み取りが発生する可能性があります。バグを修正しようとしましたが、動的に成長するスタックを実際に必要としなかったため、最終的には諦めました。スペースを割り当てようとする代わりに、Push
メソッドは単にfalse
を返します。その後、呼び出し元はスピン待機、つまりwhile(!stack->Push(value)){}
を実行できます。
#pragma once
#include <atomic>
// A lock-free stack.
// Push = single producer
// Pop = single consumer (same thread as Push)
// Steal = multiple consumer
// All methods, including Push, may fail. Re-issue the request
// if that occurs (spinwait).
template<class T, size_t capacity = 131072>
class WorkStealingStack {
public:
inline WorkStealingStack() {
_top = 1;
_bottom = 1;
}
WorkStealingStack(const WorkStealingStack&) = delete;
inline ~WorkStealingStack()
{
}
// Single producer
inline bool Push(const T& item) {
auto oldtop = _top.load(std::memory_order_relaxed);
auto oldbottom = _bottom.load(std::memory_order_relaxed);
auto numtasks = oldbottom - oldtop;
if (
oldbottom > oldtop && // size_t is unsigned, validate the result is positive
numtasks >= capacity - 1) {
// The caller can decide what to do, they will probably spinwait.
return false;
}
_values[oldbottom % capacity].store(item, std::memory_order_relaxed);
_bottom.fetch_add(1, std::memory_order_release);
return true;
}
// Single consumer
inline bool Pop(T& result) {
size_t oldtop, oldbottom, newtop, newbottom, ot;
oldbottom = _bottom.fetch_sub(1, std::memory_order_release);
ot = oldtop = _top.load(std::memory_order_acquire);
newtop = oldtop + 1;
newbottom = oldbottom - 1;
// Bottom has wrapped around.
if (oldbottom < oldtop) {
_bottom.store(oldtop, std::memory_order_relaxed);
return false;
}
// The queue is empty.
if (oldbottom == oldtop) {
_bottom.fetch_add(1, std::memory_order_release);
return false;
}
// Make sure that we are not contending for the item.
if (newbottom == oldtop) {
auto ret = _values[newbottom % capacity].load(std::memory_order_relaxed);
if (!_top.compare_exchange_strong(oldtop, newtop, std::memory_order_acquire)) {
_bottom.fetch_add(1, std::memory_order_release);
return false;
}
else {
result = ret;
_bottom.store(newtop, std::memory_order_release);
return true;
}
}
// It's uncontended.
result = _values[newbottom % capacity].load(std::memory_order_acquire);
return true;
}
// Multiple consumer.
inline bool Steal(T& result) {
size_t oldtop, newtop, oldbottom;
oldtop = _top.load(std::memory_order_acquire);
oldbottom = _bottom.load(std::memory_order_relaxed);
newtop = oldtop + 1;
if (oldbottom <= oldtop)
return false;
// Make sure that we are not contending for the item.
if (!_top.compare_exchange_strong(oldtop, newtop, std::memory_order_acquire)) {
return false;
}
result = _values[oldtop % capacity].load(std::memory_order_relaxed);
return true;
}
private:
// Circular array
std::atomic<T> _values[capacity];
std::atomic<size_t> _top; // queue
std::atomic<size_t> _bottom; // stack
};
完全な要点(単体テストを含む) テストは強力なアーキテクチャ(x86/64)でのみ実行しました。弱いアーキテクチャに関しては、これを使用しようとすると、マイレージが異なる場合があります。ネオン/ PPC。
作業タスクを小さな単位に分割すると、そもそも作業を盗む必要がなくなりますか?