web-dev-qa-db-ja.com

キャッシュラインサイズに合わせる方法とタイミング

C++で記述されたDmitry Vyukovの優れた境界付きmpmcキュー内: http://www.1024cores.net/home/lock-free-algorithms/queues/bounded-mpmc-queue

彼はいくつかのパディング変数を追加します。これは、パフォーマンスのためにキャッシュラインに合わせるためだと思います。

いくつか質問があります。

  1. なぜこのように行われるのですか?
  2. 常に機能する移植可能な方法ですか?
  3. どのような場合に、代わりに__attribute__ ((aligned (64)))を使用するのが最善でしょう。
  4. バッファポインタの前のパディングがパフォーマンスに役立つのはなぜですか?キャッシュにロードされるのはポインタだけではないので、実際にはポインタのサイズだけですか?

    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の下で機能しますか?

57
Matt

このようにして、異なるフィールドを変更する異なるコアが、キャッシュ間でそれらの両方を含むキャッシュラインをバウンスする必要がないようにします。一般に、プロセッサがメモリ内の一部のデータにアクセスするには、それを含むキャッシュライン全体がそのプロセッサのローカルキャッシュに存在する必要があります。そのデータを変更する場合、そのキャッシュエントリは通常、システム内のキャッシュ内の唯一のコピーでなければなりません(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++でも同じ概念が適用されます。

41
Phil Miller

割り込みまたは高性能データ読み取りを処理する場合、キャッシュライン境界(通常はキャッシュラインあたり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関数を使用します。 最適な動作が保証されない であるため、この関数を使用しないことをお勧めします。キャッシュラインに合わせるための最速の方法は次のとおりです。これは筆者であり、これはシャムレスプラグです。

Kabuki Toolkitのメモリアライメントアルゴリズム

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
}
3
user2356685