C++11 で記述された、単一プロデューサー、単一コンシューマー、ウェイトフリーおよびロックフリーの固定サイズのキュー。この実装は、 boost::lockfree::spscおよびfolly::ProducerConsumerQueueの両方よりも高速です。
SPSCQueue< int > q ( 1 );
auto t = std::thread([&] {
while (!q. front ());
std::cout << *q. front () << std::endl;
q. pop ();
});
q.push( 1 );
t.join();
完全な例については、 src/SPSCQueueExample.cpp
を参照してください。
SPSCQueue<T>(size_t capacity);
容量capacity
を持つタイプT
の項目を保持するSPSCqueue
を作成します。容量は少なくとも 1 必要です。
void emplace(Args &&... args);
インプレース構築を使用してアイテムをキューに追加します。キューがいっぱいの場合はブロックします。
bool try_emplace(Args &&... args);
インプレース構築を使用してアイテムをキューに入れてみます。成功した場合はtrue
返し、キューがいっぱいの場合はfalse
返します。
void push(const T &v);
コピー構築を使用してアイテムをキューに追加します。キューがいっぱいの場合はブロックします。
template <typename P> void push(P &&v);
移動構築を使用してアイテムをキューに追加します。 std::is_constructible<T, P&&>::value == true
の場合にのみオーバーロード解決に参加します。キューがいっぱいの場合はブロックします。
bool try_push(const T &v);
コピー構築を使用してアイテムをキューに入れてみます。成功した場合はtrue
返し、キューがいっぱいの場合はfalse
返します。
template <typename P> bool try_push(P &&v);
移動構築を使用してアイテムをキューに入れてみます。成功した場合はtrue
返し、キューがいっぱいの場合はfalse
返します。 std::is_constructible<T, P&&>::value == true
の場合にのみオーバーロード解決に参加します。
T *front();
ポインタをキューの先頭に戻します。キューが空の場合はnullptr
を返します。
void pop();
キューの最初の項目をデキューします。 Pop を呼び出す前に、キューが空ではないことを確認する必要があります。これは、 front()
がpop()
を呼び出す前に非nullptr
を返さなければならないことを意味します。 std::is_nothrow_destructible<T>::value == true
が必要です。
size_t size();
キュー内で使用可能なアイテムの数を返します。
bool empty();
キューが現在空の場合は true を返します。
単一の書き込みスレッドのみがエンキュー操作を実行でき、単一の読み取りスレッドのみがデキュー操作を実行できます。それ以外の使用は無効です。
このライブラリは、標準のカスタム アロケータ インターフェイスを介したカスタム割り当てのサポートに加えて、標準プロポーザル P0401R3 アロケータ インターフェイスでのサイズ フィードバックの提供もサポートしています。これにより、割り当てられたスペースを無駄にすることなく、巨大なページを便利に使用できるようになります。サイズ フィードバックの使用は、C++17 が有効な場合にのみサポートされます。
現在、ライブラリにはヒュージ ページ アロケータは含まれていません。これは、ヒュージ ページを割り当てるための API がプラットフォームに依存し、ヒュージ ページ サイズと NUMA 認識の処理がアプリケーション固有であるためです。
以下は、Linux 用の巨大ページ アロケーターの例です。
# include < sys/mman.h >
template < typename T> struct Allocator {
using value_type = T;
struct AllocationResult {
T *ptr;
size_t count;
};
size_t roundup ( size_t n) { return (((n - 1 ) >> 21 ) + 1 ) << 21 ; }
AllocationResult allocate_at_least ( size_t n) {
size_t count = roundup ( sizeof (T) * n);
auto p = static_cast <T *>( mmap ( nullptr , count, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
- 1 , 0 ));
if (p == MAP_FAILED) {
throw std::bad_alloc ();
}
return {p, count / sizeof (T)};
}
void deallocate (T *p, size_t n) { munmap (p, roundup ( sizeof (T) * n)); }
};
Linux でヒュージ ページを使用する方法の完全な例については、 src/SPSCQueueExampleHugepages.cpp
を参照してください。
基礎となる実装はリング バッファーに基づいています。
誤った共有に関する問題が発生しないように注意が払われています。先頭と末尾のインデックスは整列され、フォールス シェアリング範囲 (キャッシュ ライン サイズ) に合わせてパディングされます。さらに、スロット バッファーの先頭と末尾にフォールス シェアリング範囲が埋め込まれ、隣接する割り当てとのフォールス シェアリングが防止されます。
この実装は、ヘッド インデックスとテール インデックスをそれぞれライターとリーダーにローカルにキャッシュすることにより、一般的な同時リング バッファーよりも高いスループットを実現します。キャッシュにより、キャッシュ コヒーレンシ トラフィックの量が削減されるため、スループットが向上します。
その仕組みを理解するには、まずキャッシュがない場合の読み取り操作を考えてみましょう。ヘッド インデックス (読み取りインデックス) を更新する必要があるため、そのキャッシュ ラインは排他的状態で L1 キャッシュにロードされます。キューが空ではないことを確認するためにテール (書き込みインデックス) を読み取る必要があるため、共有状態で L1 キャッシュにロードされます。キュー書き込み操作ではヘッド インデックスを読み取る必要があるため、書き込み操作ではヘッド インデックス キャッシュ ラインを排他状態に戻すためにある程度のキャッシュ コヒーレンシ トラフィックが必要になる可能性があります。最悪の場合、読み取りおよび書き込み操作ごとに 1 つのキャッシュ ラインが共有から排他に移行することになります。
次に、末尾インデックスをキャッシュするキュー リーダーを検討します。キャッシュされた末尾インデックスがキューが空であることを示している場合、末尾インデックスをキャッシュされた末尾インデックスにロードします。キューが空ではなかった場合、キャッシュされた末尾インデックスまでの複数の読み取り操作は、ライターの末尾インデックス キャッシュ ラインの排他状態を盗むことなく完了できます。したがって、キャッシュ コヒーレンシのトラフィックが減少します。キュー書き込み操作についても同様の議論を行うことができます。
この実装では、キューがいっぱいであることを示すために追加のキュー スロットを割り当てる代わりに、任意の 2 のべき乗以外の容量が可能になります。追加のキュー スロットのためにストレージを無駄にしたくない場合は、別の実装を使用する必要があります。
参考文献:
ロックフリーのアルゴリズムをテストするのは困難です。実装をテストするために 2 つのアプローチを使用しています。
スループット ベンチマークは、 int
項目のキューの 2 つのスレッド間のスループットを測定します。
レイテンシ ベンチマークは、 int
項目の 2 つのキューを使用して通信する 2 つのスレッド間の往復時間を測定します。
AMD Ryzen 9 3900X 12 コア プロセッサのベンチマーク結果では、2 つのスレッドが同じチップレット上の異なるコアで実行されています。
列 | スループット (ops/ms) | レイテンシ RTT (ns) |
---|---|---|
SPSCキュー | 362723 | 133 |
boost::lockfree::spsc | 209877 | 222 |
folly::ProducerConsumerQueue | 148818 | 147 |
SPSCQueue は次の論文で引用されています。
このプロジェクトは Erik Rigtorp <[email protected]> によって作成されました。