Skip to main content

概要

XBOX One と XBOX Series X|Sで確立された基礎の上に、この記事ではプロセッサ コア間で共有されるメモリに対する、さまざまな読み取り/書き込み操作に関連するパフォーマンス上の問題とコストについて解説します。XBOX One ファミリおよび XBOX Series コンソールの CPU のアーキテクチャ、機能、パフォーマンスをおさらいするには、上述の記事をご覧ください。 はじめに 共有データ データ共有のコスト 推奨事項 付録: コード

はじめに


XBOX One と XBOX Series X|S では、L1 キャッシュ ミスに関連するコストについて説明されています。L1 キャッシュ ミスの最も一般的なパターンの 1 つは、CPU コア間で共有されるアドレスへの書き込みです。たとえば、複数のスレッド間で共有されるデータです。この記事の目的は、コア間で書き込み可能なデータを共有する場合に生じるペナルティに関する相対的な数値を示すことです。 私たちのテストからの主な結論は、コアやスレッド間でデータを共有するよりも、読み取り/書き込み操作におけるデータを個々のコアに限定するほうが好ましいということです。コア間でデータを共有すると、コードの処理は高速化されるのではなく、逆に遅くなります。 XBOX One ファミリは Jaguar プロセッサを、XBOX Series コンソールは Hercules プロセッサを使用しています。これらのプロセッサはどちらも、強力なメモリ モデルとキャッシュ コヒーレンシを維持するために MOESI プロトコル - Wikipedia を使用しています。各キャッシュ ラインは、次の 5 つの状態のいずれかを取り得ます。
  • Modified
    • このプロセッサがキャッシュ ラインの唯一の有効なコピーを保持しており、変更を加えています。
    • キャッシュ ラインはメモリと一致していません。
  • Owned
    • このプロセッサが唯一の有効なコピーを保持しており、変更を加えています。
    • 他のプロセッサは、読み取り専用のコピーを保持している可能性があります。
  • Exclusive
    • プロセッサがキャッシュ ラインの唯一のコピーを保持しています。
    • キャッシュ ラインの内容はメモリと一致しています。
  • Shared
    • このプロセッサが、キャッシュ ラインの複数のコピーのうちの 1 つを保持しています。
    • 変更が加えられている場合は、別のプロセッサが Owned 状態でこのキャッシュ ラインを保持している可能性があります。
  • Invalid
    • このキャッシュ ラインは無効であり、アクセス前にフェッチする必要があります。
AMD の実装に関する詳細については、AMD Developer Central にある AMD64 Architecture Programmer’s Manual Volume 2: System Programming を参照してください。

共有データ


データの共有はいくつかの方法で発生し得ます。より分かりやすい例の 1 つは、構造体の参照カウントなど、変数を直接共有する場合です。たとえば、関数呼び出しの引数として shared_ptr を介して各スレッドがコピーを作成すると、データが変更されやすくなります。この操作だけでもリソースの競合が発生します。 もう 1 つのデータ共有の形態である「フォールス シェアリング」は、検出が難しいものです。フォールス シェアリングは、2 つのメモリ アドレスが同じキャッシュ ラインに解決される場合に発生します。Jaguar プロセッサと Hercules プロセッサの L1 キャッシュ ラインはどちらも 64 バイトのサイズで、64 バイト境界にアラインされているため、同じ 64 バイト ブロック内にある 2 つの整数は共有されているとみなされます。たとえば、次の 2 つのデータ構造について考えてみましょう。
threadOne、threadTwo、threadThree、および threadFour のデータは、スレッドごとに固有です。スレッド 1 は threadOne データのみを、スレッド 2 は threadTwo データのみを操作します。これら 2 つのデータ構造を比較した場合、単純な書き込み操作だけでもマルチスレッド処理に切り替えると、OuterClassSlow は OuterClassFast よりも最大 20 倍遅く動作する可能性があります。これは、OuterClassSlow のデータがすべて、フォールス シェアリングの結果として同じキャッシュ ラインを共有するためです。それらはメモリ上で隣接しています。これは、構造体の配列 (AoS) と配列の構造体 (SoA) の違いの例です。

データ共有のコスト


テスト プロファイル

テスト プロファイルは、次の基準に従いました。
  • テストされた命令は、生の読み取り、生の書き込み、アトミック ロード、アトミック ストア、およびアトミック Compare and Store (CAS) 操作です。各命令セットに使用されたコードは、付録: コード に記載しています。
  • テストは、すべてのスレッドが共有する 8 バイトのメモリ位置 (共有キャッシュ ライン)、または各スレッドが独自の 8 バイトのメモリ位置を持つ場合 (固有キャッシュ ライン) で実行されました。
  • 40,000 回の操作をタイトなループで実行しました。
  • ループ アンローリングを無効にするため、オプティマイザーはサイズを最小化するように設定しました。
  • 合計 100 回の実行を行い、その中央値を後述の表に示します。

単一コアでのテスト

ベースラインとして、単一コアで各操作をテストすることから始めました。 | 操作 | XBOX One | XBOX One X | XBOX Series S - 3.4 GHz | XBOX Series X|S - 3.6 GHz | XBOX Series X - 3.8 GHz | |--------------|----------:|----------:|----------:|----------:|----------:| | Raw Read | 68.78 us | 52.34 us | 11.81 us | 11.16 us | 10.56 us | | Atomic Load | 68.78 us | 52.34 us | 11.81 us | 11.16 us | 10.56 us | | Raw Write | 114.61 us | 104.64 us | 11.81 us | 11.16 us | 10.56 us | | Atomic Store | 369.55 us | 278.99 us | 207.11 us | 195.61 us | 185.31 us | | Atomic CAS | 668.79 us | 509.02 us | 203.43 us | 192.13 us | 182.03 us | まず、Jaguar プロセッサと Hercules プロセッサ間のタイミングの違いです。Hercules では最大 4 倍のパフォーマンス向上が見られます。これは、クロック速度の向上、帯域幅の向上、および同時に実行可能なメモリ操作数の増加が組み合わさった結果です。 40,000 回の直接的な読み取りと 40,000 回のアトミック ロードを比較すると、時間の差はまったくありません。これは、CPU の強力なメモリ モデルと MOESI プロトコルによるものです。コンパイラは両方の操作に対して同じ命令を生成できます。 このテストでの生の書き込みおよびアトミック ストア操作は、メモリ内の値をインクリメントしようとします。このテストでは、生の書き込みとアトミック ストアの間で 2 倍のコスト増があります。これは、アトミック ストアが xchg 命令を使用しており、これに暗黙的に lock プレフィックスが含まれているためです。lock プレフィックスを使用する命令をまたいで操作を並べ替えることができないため、結果がキャッシュにフラッシュされるまでプロセッサがストールする可能性があります。生の書き込みは mov 命令を使用します。これはプロセッサをストールさせず、mov 命令をまたいだ操作の並べ替えが自由に許可されます。 これによりアトミック Compare and Store (CAS) 操作に話が進みます。このテストの操作は、生の書き込みおよびアトミック ストア テストと同様で、メモリ内の値をインクリメントしようとします。このテストでは、アトミック ストア テストと比較可能にするために、cmpxchg 命令に lock プレフィックスが使用されています。Jaguar プロセッサでは、CAS 操作は比較のために前述の xchg よりもコストが高くなります。ただし、Hercules プロセッサでは実装が改善されており、コストはアトミック ストアと同程度に抑えられています。

複数コアでのテスト

複数コアのテストではすべて、いくつかのコア構成で 2 つのスレッドを使用しました。
  • 同じクラスタ内の、異なる物理コア上の 2 つのスレッド。
  • 異なるクラスタ内の、異なる物理コア上の 2 つのスレッド。
  • SMT が有効な場合、1 つの物理プロセッサ上の 2 つの論理コアで実行される 2 つのスレッド。
各テストで 2 回の異なる実行を行いました。最初の実行では、2 つのスレッドが同じ uint64_t アドレスを共有しました。2 回目の実行では、各スレッドが独自の uint64_t アドレスを持ち、キャッシュ ラインを共有しませんでした。このようにした理由は、キャッシュ ラインがプロセッサ間で共有された場合のパフォーマンス ペナルティをより明確にするためです。各テストで、スレッドは同時に開始され、タイミングはループの反復のみから取得されました。また、コンテキスト スイッチの可能性を可能な限り低くするため、スレッドを高優先度に設定しました。各テストで 100 回の実行を行い、その中央値の結果を後述の表に示します。 以下に示される数値はすべて、単一コアの基本テストに対する相対値であり、複数コアで実行された場合に操作がどれだけ高コストになったかを示しています。

生の読み取り/アトミック ロード

これは、メモリ位置からの単純な読み取りです。データを変更するものがないため、スレッド間で競合はありません。各スレッドは、それぞれの L1 キャッシュにデータのコピーを持っています。その結果、共有データの読み取りと固有データの読み取りの時間は同じになります。強力なメモリ モデルにより、コンパイラは通常の読み取りと同じコードをアトミック ロードに対して生成することが許可されています。これにより、各操作の時間は同じになります。 | テスト | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - SMT なし | |--------------|----------:|----------:|----------:|----------:|----------:| | 単一コア | 1.00 | 1.00 | 1.00 | 1.00 | | 同一物理コア (共有) | N/A | N/A | 1.97 | N/A | | 同一クラスタ (共有) | 1.00 | 1.00 | 1.00 | 1.00 | | クラスタ間 (共有) | 1.00 | 1.00 | 1.00 | 1.00 | | 同一物理コア (固有) | N/A | N/A | 1.97 | N/A | | 同一クラスタ (固有) | 1.00 | 1.00 | 1.00 | 1.00 | | クラスタ間 (固有) | 1.00 | 1.00 | 1.00 | 1.00 | この表からの主な結論は 2 つあります。 1 つ目は、複数のコアが同じ場所から読み取っているだけの場合、メモリからの読み取りだけではパフォーマンスに影響がないということです。各プロセッサがキャッシュに有効なコピーを持ち、複数のプロセッサがコピーを保持しているため、キャッシュ ラインは Shared 状態にあります。 2 つ目は、両方のスレッドが同じ物理コアで実行される構成では、時間が 2 倍になるということです。SMT が有効な場合、コアのリソースは 2 つのスレッドで共有されます。1 つのスレッドが利用可能なリソースをすべて活用できないことが多いため、これによりパフォーマンスの向上が得られることがあります。ただし、このテストは Load/Store ユニットと L1 キャッシュを支配するタイトなループです。単一のスレッドが Load/Store ユニットのすべてのスロットを使用できるため、2 つのスレッドはラウンド ロビン形式でこれらを共有する必要があります。実質的には、各スレッドが 2 倍の時間を要しますが、コア全体としては 2 倍の作業を行っています。全体としては、コアは同じ時間内に同じ量の作業を行っています。

生の書き込み

コア間で共有されるメモリへの単純な書き込みは、単一のコアが共有されていないメモリ位置に書き込む場合よりも大幅に遅くなります。書き込み操作により、コアはキャッシュ ラインを更新し、他のコア上のコピーを無効化します。 この特定のテストは、共有メモリ内の値のインクリメントです。つまり、コアのキャッシュに有効なコピーがない場合、メモリまたはそのアドレスに最後に書き込みを行ったコアからデータをリクエストする必要があります。これにより、2 つのコア間で「ピンポン」効果が発生します。一方のコアがデータを更新し、もう一方のコアが最初のコアからデータを読み取って更新すると、最初のコアが更新できるようになる前にそのコアからデータを読み取る必要があります。テスト ループの反復ごとに、これが交互に繰り返されます。 ピンポン効果のオーバーヘッドは、この表で確認できます。 | テスト | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - SMT なし | |--------------|----------:|----------:|----------:|----------:|----------:| | 単一コア | 1.00 | 1.00 | 1.00 | 1.00 | | 同一物理コア (共有) | N/A | N/A | 10.69 | N/A | | 同一クラスタ (共有) | 10.54 | 8.73 | 24.36 | 23.59 | | クラスタ間 (共有) | 13.61 | 12.78 | 18.89 | 23.57 | | 同一物理コア (固有) | N/A | N/A | 1.98 | N/A | | 同一クラスタ (固有) | 1.05 | 0.93 | 1.03 | 1.03 | | クラスタ間 (固有) | 1.01 | 0.84 | 1.04 | 1.03 | この表からの結論はいくつかあります。 1 つ目は、Jaguar プロセッサと Hercules プロセッサでのパフォーマンスへの相対的な影響の違いです。Hercules ではキャッシュ ラインを共有するコストが Jaguar よりも高くなります。この場合、Jaguar では他のコアからデータをリクエストするコストが 17 サイクルであるのに対し、Hercules での同じ操作のコストは 90 サイクルです。これは、Hercules のより深いキャッシュ、および Jaguar にはない L3 キャッシュの存在によるものです。 2 つ目は、共有メモリ位置と固有位置を使用する場合の違いです。この場合の操作は現在の値のインクリメント、つまり読み取り/変更/書き込み操作です。読み取り操作は、そのメモリ位置に最も最近書き込みを行ったコアによって処理される必要があります。それが同じコアでない場合、読み取りのコストは劇的に高くなります。表からわかるように、これは最大 25 倍のコストになる可能性があります。 3 つ目は、SMT が有効な単一の物理コアを 2 つのスレッドが共有する場合の相対的な差が小さいということです。更新されたキャッシュ ラインを別のコアからフェッチする必要はなく、既にローカルにあります。ただし、ストア to ロード フォワーディングは適用できません。スレッド A が値を更新すると、スレッド B がその値を使用できるようになる前に、キャッシュにフラッシュする必要があります。 4 つ目は、両方のスレッドが同じ物理コアで実行され、固有のメモリを操作する構成では、時間が 2 倍になるということです。SMT が有効な場合、コアのリソースは 2 つのスレッドで共有されます。1 つのスレッドが利用可能なリソースをすべて活用できないことが多いため、これによりパフォーマンスの向上が得られることがあります。ただし、このテストは Load/Store ユニットと L1 キャッシュを支配するタイトなループです。単一のスレッドが Load/Store ユニットのすべてのスロットを使用できるため、2 つのスレッドはラウンド ロビン形式でこれらを共有する必要があります。実質的には、各スレッドが 2 倍の時間を要しますが、コア全体としては 2 倍の作業を行っています。全体としては、コアは同じ時間内に同じ量の作業を行っています。

アトミック ストア

アトミック ストアは xchg 命令を使用し、これは暗黙的に lock フラグを通知します。xchg 命令は、対象のメモリ アドレスに対して読み取りと書き込みを行う必要があります。この間、キャッシュ ラインはロックされ、他のコアがそのキャッシュ ラインにアクセスできなくなります。これにより、xchg 命令がキャッシュにデータを書き込むまで、保留中の操作がストールする可能性があります。もう 1 つのコストは、コアが xchg 命令をまたいで操作を並べ替えることができないことです。最後のコストは、別のコアがデータを変更していた場合に発生します。その場合、そのコアからデータをフェッチする必要があります。共有キャッシュ ラインで操作している間、他のコアはこの一連のシーケンス全体を通じて待機する必要があります。 生の書き込みテストと同様に、アトミック ストア テストでも同様のピンポン効果が発生する可能性がありますが、この場合はより顕著で、単一コア テストにも影響します。 プロセッサは、ループの 1 反復以上先を投機的に実行することはできません。スレッド A が xchg 命令を実行している間、スレッド B はアクセスを待機します。スレッド A が完了するとすぐに、スレッド B が続行し、スレッド A が次のループ反復からアクセスをリクエストする前に、直ちにコピーをリクエストします。 この表は、単一コアが操作を実行する場合と比較した、xchg 命令の相対コストを示しています。 | テスト | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - SMT なし | |--------------|----------:|----------:|----------:|----------:|----------:| | 単一コア | 1.00 | 1.00 | 1.00 | 1.00 | | 同一物理コア (共有) | N/A | N/A | 2.38 | N/A | | 同一クラスタ (共有) | 6.84 | 6.85 | 4.17 | 4.04 | | クラスタ間 (共有) | 10.64 | 13.40 | 3.72 | 3.56 | | 同一物理コア (固有) | N/A | N/A | 0.98 | N/A | | 同一クラスタ (固有) | 1.00 | 1.00 | 1.00 | 1.00 | | クラスタ間 (固有) | 1.01 | 1.00 | 1.00 | 1.00 | この表のデータからの結論は 2 つあります。 共有アドレスを使用した場合の相対時間は、生の書き込みに比べてアトミック ストアではそれほど極端ではありません。これは、このテストの大部分のコストが既に単一コア テストで支払われているためです。ただし、Hercules ではアトミック ストアの全体的なコストは、生の書き込みの 2 ~ 4 倍のコストであり、Jaguar では最大 13 倍のコストです。 2 つ目は、Hercules では lock 操作の実装が改善されており、この場合、同一クラスタ テストとクラスタ間テストがほぼ同じ時間かかるということです。Jaguar プロセッサでは、データがクラスタ間で共有されて lock 操作が使用される場合、最大 2 倍のコストになる可能性があります。

アトミック Compare and Store (CAS)

このテストのアトミック Compare and Store (CAS) 操作は、lock プレフィックス付きの cmpxchg 命令を使用します。つまり、操作の実行中はキャッシュ ラインがロックされ、他のプロセッサがアクセスできません。これは、アトミック ストア テストにおける xchg 命令の動作と同じです。 このテストでは、CAS 操作はアトミック ストア操作と同じ計算を実行します。共有または固有のメモリ内の値をインクリメントしようとします。唯一の違いは、CAS 操作は指定された 3 番目の値と等しい場合にのみメモリ位置に書き込むことです。 | テスト | XBOX One | XBOX One X | XBOX Series X|S - SMT | XBOX Series X|S - SMT なし | |--------------|----------:|----------:|----------:|----------:|----------:| | 単一コア | 1.00 | 1.00 | 1.00 | 1.00 | | 同一物理コア (共有) | N/A | N/A | 2.91 | N/A | | 同一クラスタ (共有) | 4.32 | 4.32 | 4.01 | 4.24 | | クラスタ間 (共有) | 10.12 | 14.80 | 3.52 | 3.45 | | 同一物理コア (固有) | N/A | N/A | 0.91 | N/A | | 同一クラスタ (固有) | 1.00 | 1.00 | 1.00 | 1.00 | | クラスタ間 (固有) | 1.00 | 1.00 | 1.00 | 1.00 | アトミック ストア テストとアトミック Compare and Store テストの両方が lock プレフィックスを使用しているため、その相対コストは同じであり、理由も同じです。 共有アドレスを使用した場合の単一スレッドとマルチスレッド操作の相対時間はそれほど極端ではありません。これは、このテストの大部分のコストが既に単一コア テストで支払われているためです。ただし、Hercules ではデータが共有される場合の CAS の全体的なコストは 2 ~ 4 倍で、Jaguar では最大 14 倍のコストになります。 2 つ目は、Hercules では lock 操作の実装が改善されており、この場合、同一クラスタ テストとクラスタ間テストがほぼ同じ時間かかるということです。Jaguar プロセッサでは、データがクラスタ間で共有されて lock 操作が使用される場合、最大 2 倍のコストになる可能性があります。

推奨事項


コア間で書き込み可能なデータを共有することを避けるために従えるいくつかの重要なパターンがあります。
  • データ構造: 各スレッドが固有の構造体を操作するように、構造体の配列 (AoS) から配列の構造体 (SoA) に切り替えます。
  • 読み取り専用データを読み取り/書き込みデータから分離します。
  • パディング: 構造体をキャッシュ ライン サイズ (64 バイト) の倍数でパディングできます。これにより、各スレッドが構造体を操作する場合のフォールス シェアリングが解消されます。
  • 参照カウント (shared_ptr、weak_ptr など): これらはどちらも、オブジェクトのすべてのインスタンス間で共有される単一の参照カウント変数を持っています。
  • データの所有権を非常に明確にします。データが処理のために別のスレッドに引き渡されると、そのスレッドがデータを所有することになります。そのスレッドがデータの処理を完了するまで、データに対して何も行わないようにしてください。
  • 共有ジョブ キュー: 特にジョブが小さい場合、複数のスレッドで共有される単一のジョブ キューの使用は避けてください。代わりに、各スレッドに固有のジョブ キューを持たせることを検討してください。ワーク スティーリング アルゴリズムを使用して、スレッド間の負荷分散を助けることができます。

付録: コード


生の読み取り

アトミック ロード

生の書き込み

アトミック ストア

アトミック CAS

最終更新日 2026年8月24日