2007 年の Ulrich Drepper のWhat Every Programmer Should Know About Memoryのどの程度がまだ有効か知りたいです。また、1.0 より新しいバージョンや正誤表を見つけることができませんでした。
(Ulrich Drepper 氏自身のサイトにも PDF 形式で掲載されています: https://www.akkadia.org/drepper/cpumemory.pdf )
ベストアンサー1
PDF 形式のガイドはhttps://www.akkadia.org/drepper/cpumemory.pdfにあります。
これは依然として全体的に優れており、強く推奨されています(私だけでなく、他のパフォーマンス チューニングの専門家からも推奨されていると思います)。Ulrich (または他の誰か) が 2017 年の更新を書いてくれたら素晴らしいのですが、それは大変な作業 (ベンチマークの再実行など) になるでしょう。x86 タグwikiにある他の x86 パフォーマンス チューニングおよび SSE/asm (および C/C++) 最適化のリンクも参照してください。(Ulrich の記事は x86 に固有のものではありませんが、彼のベンチマークのほとんど (すべて) は x86 ハードウェア上で行われています。)
DRAM とキャッシュの動作に関する低レベルのハードウェアの詳細はすべて引き続き適用されます。DDR4 は、 DDR1/DDR2 (読み取り/書き込みバースト) で説明されているのと同じコマンドを使用します。DDR3/4 の改善は根本的な変更ではありません。私の知る限り、アーキテクチャに依存しないすべての内容は、たとえば AArch64 / ARM32 など、一般的に引き続き適用されます。
メモリ/L3 レイテンシがシングルスレッド帯域幅に与える影響に関する重要な詳細については、この回答のレイテンシ バウンド プラットフォーム セクションも参照してくださいbandwidth <= max_concurrency / latency
。これは、Xeon などの最新のマルチコア CPU におけるシングルスレッド帯域幅の主なボトルネックです。ただし、クアッドコアの Skylake デスクトップは、シングル スレッドで DRAM 帯域幅を最大限に利用しそうになります。このリンクには、x86 の NT ストアと通常のストアに関する非常に優れた情報が記載されています。シングルスレッド メモリ スループットに関して、Skylake が Broadwell-E より優れているのはなぜですか?が要約です。
したがって、Ulrich が6.5.8 すべての帯域幅の利用で、自分の NUMA ノードだけでなく他の NUMA ノードでもリモート メモリを使用するという提案は、メモリ コントローラの帯域幅が 1 つのコアで使用できる帯域幅を超えている現代のハードウェアでは逆効果です。おそらく、低レイテンシのスレッド間通信のために同じ NUMA ノードでメモリを大量に消費する複数のスレッドを実行し、レイテンシの影響を受けない高帯域幅の作業にはリモート メモリを使用する方が純粋なメリットがある状況を想像できるでしょう。しかし、これはかなりわかりにくいので、通常はスレッドを NUMA ノード間で分割し、ローカル メモリを使用するようにします。コアあたりの帯域幅は、最大同時実行数の制限 (以下を参照) のためにレイテンシの影響を受けやすいですが、1 つのソケット内のすべてのコアは通常、そのソケットのメモリ コントローラを飽和させる可能性があります。
(通常は)ソフトウェアプリフェッチを使用しない
大きく変わった点の 1 つは、ハードウェア プリフェッチがPentium 4 よりもはるかに優れており、かなり大きなストライドまでのストライド アクセス パターンと、複数のストリーム (たとえば、4k ページごとに 1 つの前方 / 後方) を同時に認識できることです。Intelの最適化マニュアルには、Sandybridge ファミリ マイクロアーキテクチャのさまざまなレベルのキャッシュにおける HW プリフェッチャーの詳細がいくつか記載されています。Ivybridge 以降では、新しいページでのキャッシュ ミスを待って高速スタートをトリガーするのではなく、次のページのハードウェア プリフェッチが行われます。AMD の最適化マニュアルにも同様のことが書かれていると思います。Intel のマニュアルには古いアドバイスも満載されており、その一部は P4 にしか当てはまらないことに注意してください。Sandybridge 固有のセクションは、もちろん SnB に当てはまりますが、たとえばマイクロ融合 uop のアンラミネーションは HSW で変更されましたが、マニュアルにはそれについて記載されていません。
最近の一般的なアドバイスは、古いコードからすべての SW プリフェッチを削除し、プロファイリングでキャッシュ ミスが示された場合 (およびメモリ帯域幅が飽和していない場合) のみ、プリフェッチを戻すことを検討することです。バイナリ検索の次のステップの両側をプリフェッチすると、依然として役立ちます。たとえば、次にどの要素を調べるかを決定したら、1/4 要素と 3/4 要素をプリフェッチして、中間の読み込み/チェックと並行して読み込むことができます。
別個のプリフェッチ スレッド (6.3.4) を使用するという提案は完全に時代遅れだと思います。Pentium 4 でのみ有効でした。P4 にはハイパースレッディング (2 つの論理コアが 1 つの物理コアを共有) がありましたが、同じコアで 2 つの完全な計算スレッドを実行してスループットを得るにはトレース キャッシュ (および/または順序外実行リソース) が不十分でした。ただし、最新の CPU (Sandybridge ファミリと Ryzen) ははるかに強力であり、実際のスレッドを実行するか、ハイパースレッディングを使用しない (他の論理コアをアイドル状態のままにして、ROB を分割するのではなく、単独のスレッドが完全なリソースを使用する) 必要があります。
ソフトウェア プリフェッチは常に「脆弱」です。高速化を実現するための適切な魔法のチューニング数値は、ハードウェアの詳細、場合によってはシステム負荷に依存します。早すぎると、要求負荷の前に排除されてしまいます。遅すぎると役に立ちません。このブログ記事では、問題の非シーケンシャル部分をプリフェッチするために Haswell でソフトウェア プリフェッチを使用する興味深い実験のコードとグラフを示します。プリフェッチ命令を適切に使用する方法も参照してください。NT プリフェッチは興味深いものですが、L1 からの早期排除は L2 だけでなく L3 または DRAM まで進む必要があることを意味するため、さらに脆弱です。パフォーマンスを最後まで必要とし、特定のマシンに合わせてチューニングできる場合は、シーケンシャル アクセスに対してソフトウェア プリフェッチを検討する価値がありますが、メモリのボトルネックに近づきながら実行する ALU 作業が十分にある場合は、それでも速度が低下する可能性があります。
キャッシュ ライン サイズは依然として 64 バイトです。(L1D の読み取り/書き込み帯域幅は非常に高く、最新の CPU は、すべてが L1D にヒットする場合、クロックごとに 2 つのベクトル ロード + 1 つのベクトル ストアを実行できます。「キャッシュがこれほど高速になる方法」を参照してください。) AVX512 では、ライン サイズ = ベクトル幅であるため、1 つの命令でキャッシュ ライン全体をロード/ストアできます。したがって、256b AVX1/AVX2 では 1 つおきにロード/ストアするのではなく、すべての不整列ロード/ストアがキャッシュ ライン境界を越えるため、L1D にない配列をループしても速度が低下することはほとんどありません。
実行時にアドレスがアラインされている場合、アラインされていないロード命令のペナルティはゼロですが、コンパイラ (特に gcc) は、アラインメントの保証を認識している場合、自動ベクトル化時に優れたコードを作成します。実際には、アラインされていない操作は一般的に高速ですが、ページ分割は依然として悪影響を及ぼします (ただし、Skylake では影響ははるかに小さく、レイテンシは 100 サイクルに対して約 11 サイクルしか追加されませんが、それでもスループットのペナルティとなります)。
Ulrich が予測したように、最近のマルチソケットシステムはすべて NUMA です。統合メモリ コントローラが標準で、外部ノースブリッジはありません。ただし、マルチコア CPU が普及しているため、SMP はもはやマルチソケットを意味しません。Nehalem から Skylake までの Intel CPU は、コア間の一貫性のバックストップとして、大規模な包括的なL3 キャッシュを使用しています。AMD CPU は異なりますが、詳細についてはよくわかりません。
Skylake-X (AVX512) には包括的な L3 がなくなりましたが、すべてのコアにスヌープを実際にブロードキャストすることなく、チップ上のどこに何がキャッシュされているか (キャッシュされている場合はどこにキャッシュされているか) を確認できるタグ ディレクトリはまだあると思います。SKX はリング バスではなくメッシュを使用しますが、残念ながら、以前のマルチコア Xeon よりもレイテンシがさらに悪化しています。
基本的に、メモリ配置の最適化に関するアドバイスはすべて適用されますが、キャッシュ ミスや競合を回避できない場合に何が起こるかという詳細は異なります。
6.1 キャッシュのバイパス- SSE4.1 movntdqa
( _mm_stream_load_si128)
NT ロードは、WC メモリ領域でのみ何かを行います。malloc
/new
またはmmap
(WB メモリ属性 = ライトバック キャッシュ可能) から取得する通常のメモリでは、movntqda
キャッシュをバイパスせずに通常の SIMD ロードと同じように実行されます。ただし、追加の ALU uop が必要になります。 私の知る限り、これはこの記事が書かれた当時の CPU でも当てはまり、ガイドでは珍しい間違いになっています。 NT ストアとは異なり、NT ロードでは、領域の通常のメモリ順序付けルールがオーバーライドされません。また、一貫性を尊重する必要があるため、WB キャッシュ可能な領域のキャッシュを完全にスキップすることはできず、データは書き込み時に他のコアが無効化できる場所に置く必要があります。 ただし、SSE4.1 は第 2 世代 Core 2 まで導入されなかったため、シングルコア CPU には搭載されていませんでした。
NTプリフェッチ( prefetchnta
) はキャッシュ汚染を最小限に抑えることができますが、L1d キャッシュと、包括的 L3 キャッシュを備えた Intel CPU の L3 の片道は依然としていっぱいになります。しかし、これは脆弱で調整が困難です。プリフェッチ距離が短いと、おそらく NT の側面を無効にする負荷がかかり、長すぎると、データは使用前に削除されます。また、L2 ではなく、おそらく L3 にさえないため、DRAM まで見逃される可能性があります。プリフェッチ距離は、自分のコードだけでなく、システムと他のコードのワークロードに依存するため、これは問題です。
関連している:
- PREFETCH 命令と PREFETCHNTA 命令の違い
- 非一時的ロードとハードウェア プリフェッチャーは連携して動作しますか?
- 高速ビデオ デコード フレーム バッファーのコピー-ビデオ RAMからの
movntdqa
読み取りにおけるSSE4.1 の使用例を説明する Intel のホワイト ペーパー。
6.4.2 アトミック オペレーション: CAS 再試行ループがハードウェア アービトレーションよりも 4 倍悪いことを示すベンチマークは、lock add
おそらく最大の競合ケースを反映しています。ただし、実際のマルチスレッド プログラムでは、同期は最小限に抑えられます (コストがかかるため)。そのため競合は少なく、CAS 再試行ループは通常、再試行することなく成功します。
C++11は(または戻り値が使用されている場合)std::atomic
fetch_add
にコンパイルされますが、 ed 命令では実行できないことを CAS を使用して実行するアルゴリズムは、通常は問題にはなりません。同じ場所へのアトミック アクセスと非アトミック アクセスを混在させたい場合を除き、 gcc の従来の組み込み関数または新しい組み込み関数の代わりにC++11または C11 を使用してください...lock add
lock xadd
lock
std::atomic
stdatomic
__sync
__atomic
8.1 DWCAS ( cmpxchg16b
) : gcc にこれを出力させることはできますが、オブジェクトの半分だけを効率的にロードしたい場合は、醜いunion
ハックが必要になります: c++11 CAS で ABA カウンターを実装するにはどうすればよいですか? (DWCAS と 2 つの別々のメモリ位置の DCASを混同しないでください。DCAS のロックフリー アトミック エミュレーションは DWCAS では不可能ですが、トランザクション メモリ (x86 TSX など) では可能です。)
8.2.4 トランザクション メモリ: 数回の誤った開始 (リリースされた後、まれに発生するバグのためマイクロコードの更新によって無効化) を経て、Intel は最新モデルの Broadwell とすべての Skylake CPU でトランザクション メモリを動作させています。この設計は、David Kanter が Haswell について説明したとおりです。ロック省略法を使用して、通常のロックを使用する (およびフォールバックできる) コードを高速化したり (特に、コンテナーのすべての要素に対して単一のロックを使用すると、同じクリティカル セクション内の複数のスレッドが衝突することがほとんどない)、トランザクションを直接認識するコードを記述したりできます。
更新: そして現在、Intel はマイクロコードの更新により、後期の CPU (Skylake を含む) でロック省略を無効にしています。TSX の RTM (xbegin / xend) 非透過部分は、OS が許可している場合は引き続き動作しますが、TSX 全般は真剣にチャーリー ブラウンのフットボールに変わりつつあります。
- Spectre 緩和策により、ハードウェア ロック除去は永久に解消されましたか? (はい、ただし、Spectre ではなく、MDS タイプのサイドチャネル脆弱性 ( TAA ) が原因です。私の理解では、更新されたマイクロコードは HLE を完全に無効にします。その場合、OS は HLE ではなく RTM のみを有効にできます。)
7.5 Hugepages : 匿名の透過的な hugepages は、手動で hugetlbfs を使用しなくても Linux 上で適切に動作します。割り当ては 2MiB 以上にし、2MiB の境界を揃えます (たとえばposix_memalign
、 またはaligned_alloc
のときに失敗するという愚かな ISO C++17 要件を強制しないsize % alignment != 0
)。
2MiB に揃えられた匿名割り当てでは、デフォルトで hugepages が使用されます。一部のワークロード (たとえば、大きな割り当てを行った後、しばらくその割り当てを使用し続けるワークロード) では、
echo defer+madvise >/sys/kernel/mm/transparent_hugepage/defrag
4k ページにフォールバックするのではなく、必要に応じてカーネルに物理メモリをデフラグさせることでメリットが得られる場合があります (カーネルのドキュメントを参照してください)。madvise(MADV_HUGEPAGE)
大きな割り当て (できれば 2MiB に揃えたまま) を行った後に使用して、カーネルが停止してデフラグをすぐに実行するように強く促します。defrag = はalways
ほとんどのワークロードに対して積極的すぎるため、TLB ミスを節約するよりもページのコピーに時間がかかります (kcompactd の方が効率的かもしれません)。
ちなみに、Intel と AMD は 2M ページを「ラージ ページ」と呼び、「huge」は 1G ページにのみ使用されます。Linux では、標準サイズより大きいすべてのものに「hugepage」が使用されます。
(32 ビット モードのレガシー (非 PAE) ページ テーブルでは、次に大きいサイズは 4M ページのみで、エントリがよりコンパクトな 2 レベルのページ テーブルのみでした。次のサイズは 4G でしたが、これはアドレス空間全体であり、その「レベル」の変換は CR3 制御レジスタであり、ページ ディレクトリ エントリではありません。これが Linux の用語に関連しているかどうかはわかりません。)
付録 B: Oprofile : Linux はperf
、ほとんどを置き換えましたoprofile
。/にperf list
は、perf stat -e event1,event2 ...
HW パフォーマンス カウンターをプログラムするための便利な方法のほとんどに名前が付いています。
perf stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out
数年前は、イベント名をコードに変換するにはラッパーocperf.py
が必要でしたが、現在ではperf
その機能が組み込まれています。
使用例については、「x86 の MOV は本当に「無料」ですか? なぜこれをまったく再現できないのですか?」を参照してください。