C++で記述されたDmitry Vyukovの優れた境界付きmpmcキュー内: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue
彼はいくつかのパディング変数を追加します。これは、パフォーマンスのためにキャッシュラインに合わせるためだと思います。
いくつか質問があります。
__attribute__ ((aligned (64)))
を使用するのが最善でしょう。バッファポインタの前のパディングがパフォーマンスに役立つのはなぜですか?キャッシュにロードされるのはポインタだけではないので、実際にはポインタのサイズだけですか?
static size_t const cacheline_size = 64;
typedef char cacheline_pad_t [cacheline_size];
cacheline_pad_t pad0_;
cell_t* const buffer_;
size_t const buffer_mask_;
cacheline_pad_t pad1_;
std::atomic<size_t> enqueue_pos_;
cacheline_pad_t pad2_;
std::atomic<size_t> dequeue_pos_;
cacheline_pad_t pad3_;
このコンセプトはcccのgccの下で機能しますか?
このようにして、異なるフィールドを変更する異なるコアが、キャッシュ間でそれらの両方を含むキャッシュラインをバウンスする必要がないようにします。一般に、プロセッサがメモリ内の一部のデータにアクセスするには、それを含むキャッシュライン全体がそのプロセッサのローカルキャッシュに存在する必要があります。そのデータを変更する場合、そのキャッシュエントリは通常、システム内のキャッシュ内の唯一のコピーでなければなりません(MESI/MOESIスタイルの排他モードキャッシュコヒーレンスプロトコル)。別々のコアが同じキャッシュラインに存在する異なるデータを変更しようとするため、そのライン全体を前後に移動するのに時間を浪費する場合、それはfalse sharingとして知られています。
特定の例では、1つのコアがエントリをキューに入れることができます(読み取り(共有)buffer_
および書き込み(排他的)のみenqueue_pos_
)別のデキュー中(共有buffer_
および排他的dequeue_pos_
)どちらか一方のコアが、他方が所有するキャッシュラインで停止することはありません。
先頭のパディングは、buffer_
およびbuffer_mask_
は、2つのラインに分割されるのではなく、同じキャッシュラインに配置されるため、アクセスに2倍のメモリトラフィックが必要になります。
この手法が完全に移植可能かどうかはわかりません。 仮定は、各 (コメントを参照)cacheline_pad_t
自体は64バイト(そのサイズ)のキャッシュライン境界に揃えられるため、後続のものは次のキャッシュラインに配置されます。私が知る限り、CおよびC++言語標準は、構造全体のこれのみを必要とするため、メンバーのアライメント要件に違反することなく、配列内で適切に動作できます。
attribute
アプローチはよりコンパイラ固有ですが、パディングは各要素を完全なキャッシュラインに切り上げることに制限されるため、この構造のサイズを半分に削減できます。これらがたくさんある場合、それは非常に有益です。
CおよびC++でも同じ概念が適用されます。
割り込みまたは高性能データ読み取りを処理する場合、キャッシュライン境界(通常はキャッシュラインあたり64バイト)に合わせる必要がある場合があり、プロセス間ソケットを使用する場合は使用が必須です。プロセス間ソケットでは、複数のキャッシュラインに分散できない制御変数、またはDDR RAMワード、そうでなければL1、L2など、キャッシュまたはDDR RAMは、ローパスフィルターとして機能し、割り込みデータをフィルターで除外します!THAT IS BAD !!!これは、アルゴリズムが適切で潜在的な可能性がある場合に、奇妙なエラーが発生することを意味しますあなたを狂わせるために!
DDR RAMはほとんど常に128ビットワード(DDR RAMワード)、つまり16バイトで読み取られるため、リングバッファー変数は複数のDDR RAMワード。一部のシステムは64ビットDDR RAMワードを使用しており、技術的には32ビットDDR RAM 16ビットCPU上のワードですが、この状況ではSDRAMを使用します。
また、高性能アルゴリズムでデータを読み取るときに使用するキャッシュラインの数を最小限に抑えることにも関心があるかもしれません。私の場合、世界最速の整数から文字列へのアルゴリズム(以前の最速アルゴリズムより40%高速)を開発し、世界最速の浮動小数点アルゴリズムであるGrisuアルゴリズムの最適化に取り組んでいます。浮動小数点数を印刷するには整数を印刷する必要があるため、Grisuを最適化するために実装した最適化の1つは、Grisuのルックアップテーブル(LUT)を正確に15のキャッシュラインにキャッシュすることです。それが実際にそのように整列しているのはかなり奇妙です。これは、.bssセクション(つまり、静的メモリ)からLUTを取得し、それらをスタック(またはヒープですが、スタックがより適切です)に配置します。私はこれをベンチマークしていませんが、育てるのは良いことであり、これについて多くのことを学びました。値をロードする最も速い方法は、dキャッシュではなくiキャッシュからロードすることです。違いは、iキャッシュが読み取り専用であり、読み取り専用であるためにキャッシュラインがはるかに大きいことです(教授がかつて引用したのは2KBでした)。そのため、実際には、次のような変数をロードするのではなく、配列のインデックス付けからパフォーマンスを低下させます。
int faster_way = 12345678;
より遅い方法とは対照的に:
int variables[2] = { 12345678, 123456789};
int slower_way = variables[0];
違いは、int variable = 12345678
は関数の先頭からiキャッシュの変数にオフセットすることでiキャッシュラインからロードされるのに対し、slower_way = int[0]
は小さいd-はるかに遅い配列のインデックス作成を使用したキャッシュライン。私が発見したばかりのこの微妙な点は、実際、他の多くの整数から文字列へのアルゴリズムを遅くしています。これは、そうでない場合に読み取り専用データをキャッシュ調整することで最適化する可能性があるためです。
通常、C++では、std::align
関数を使用します。 最適な動作が保証されない であるため、この関数を使用しないことをお勧めします。キャッシュラインに合わせるための最速の方法は次のとおりです。これは筆者であり、これはシャムレスプラグです。
namespace _ {
/* Aligns the given pointer to a power of two boundaries with a premade mask.
@return An aligned pointer of typename T.
@brief Algorithm is a 2's compliment trick that works by masking off
the desired number of bits in 2's compliment and adding them to the
pointer.
@param pointer The pointer to align.
@param mask The mask for the Least Significant bits to align. */
template <typename T = char>
inline T* AlignUp(void* pointer, intptr_t mask) {
intptr_t value = reinterpret_cast<intptr_t>(pointer);
value += (-value ) & mask;
return reinterpret_cast<T*>(value);
}
} //< namespace _
// Example calls using the faster mask technique.
enum { kSize = 256 };
char buffer[kSize + 64];
char* aligned_to_64_byte_cache_line = AlignUp<> (buffer, 63);
char16_t* aligned_to_64_byte_cache_line2 = AlignUp<char16_t> (buffer, 63);
そして、こちらがより速いstd :: alignの置換です:
inline void* align_kabuki(size_t align, size_t size, void*& ptr,
size_t& space) noexcept {
// Begin Kabuki Toolkit Implementation
intptr_t int_ptr = reinterpret_cast<intptr_t>(ptr),
offset = (-int_ptr) & (align - 1);
if ((space -= offset) < size) {
space += offset;
return nullptr;
}
return reinterpret_cast<void*>(int_ptr + offset);
// End Kabuki Toolkit Implementation
}