プログラマーが信じているCPUキャッシュの神話

IntelとSunで5年間キャッシュの問題を扱ってきたコンピューターエンジニアとして、 キャッシュの一貫性について少し理解しています。 これは、私が大学に戻って勉強しなければならなかった最も難しい概念の1つです。 しかし、実際に習得するとすぐに、システム設計の原則についての理解が深まります。



あなたは疑問に思うかもしれません:なぜソフトウェア開発者はCPUのキャッシングメカニズムを考える必要がありますか? 答えます。 一方では、キャッシュ一貫性の概念からの多くの概念は、 分散システムおよびDBMS分離レベルで直接適用できます 。 たとえば、ハードウェアキャッシュで一貫性の実装を提示すると、一貫性のモデルの違い、つまり強い一貫性と最終的な一貫性の違いをよりよく理解するのに役立ちます。 ハードウェアの研究と原則を使用して、分散システムの一貫性を確保するための最善の方法に関する新しいアイデアを思い付くかもしれません。



一方、キャッシュについての誤解は、特に並行性と競合状態に関しては、しばしば誤った主張につながります。 たとえば、 「キャッシュ内の異なるコアは異なる/時代遅れの値を持っている可能性があるため、彼らはしばしば並列プログラミングの難しさについて話します 。 または、 「共有データのローカルキャッシュを防止」し「メインメモリへの読み取り/書き込みのみ を強制するには、Javaなどの言語のvolatile修飾子が必要です。



このような誤解はほとんど無害です(そして役に立つことさえあります)が、デザインの決定が下手になります。 たとえば、開発者は、シングルコアシステムを使用する場合、前述の同時実行エラーを回避できると考えるかもしれません。 実際、対応する同時実行構文が使用されていない場合、シングルコアシステムでさえ同時実行エラーのリスクがあります。



または別の例。 揮発性変数が毎回メインメモリから実際に読み書きされる場合、それらは非常に遅くなりますメインメモリ内のリンクはL1キャッシュ内の200倍遅くなります 。 実際、 (Javaでの)volatile-readsはL1 cacheからのものと同じくらい生産的であることが多く 、これはvolatileがメインメモリのみに読み取り/書き込みを強制するという神話を覆します。 パフォーマンスの問題が原因で揮発性を回避した場合、上記の誤解の犠牲になる可能性があります。



一貫性の重要性



しかし、異なるデータのコアが同じデータのコピーを保存する独自のキャッシュを持っている場合、これはエントリの不一致につながりませんか? 回答:Intelのような最新のx86プロセッサのハードウェアキャッシュは常に同期されます。 多くの開発者が考えるように、これらのキャッシュは単なるメモリの単なるブロックではありません。 それどころか、非常に複雑なプロトコルとキャッシュ間の相互作用の組み込みロジックにより、すべてのスレッドで一貫性が確保されます。 そして、これらはすべてハードウェアレベルで行われます。つまり、ソフトウェア、コンパイラ、システムの開発者は、それについて考える必要はありません。



「同期」キャッシュの意味を簡単に説明します。 多くのニュアンスがありますが、最大の簡素化では:システム内の2つの異なるストリームが同じメモリアドレスから読み取る場合、それらが同時に異なる値読み取ることはありません。



一貫性のあるキャッシュが上記の規則にどのように違反するかを示す簡単な例として、 このチュートリアルの最初のセクションを参照してください 。 チュートリアルで説明されているように動作する最新のx86プロセッサはありませんが、バグのあるプロセッサは確かに動作します。 この記事では、1つの簡単な目標に焦点を当てます。このような矛盾を防ぐことです。



キャッシュ間の一貫性を確保するための最も一般的なプロトコルは、MESIプロトコルとして知られています 。 各プロセッサには独自のMESI実装があり、異なるオプションにはそれぞれの利点、トレードオフ、および固有のバグに対する機会があります。 ただし、これらにはすべて共通の原則があります。キャッシュ内のデータの各行には、次のいずれかの状態がマークされます。



  1. 変更された条件(M)。
    1. これらのデータは変更され、メインメモリとは異なります。
    2. これらのデータは真実の源であり、他のすべての源は時代遅れです。
  2. 排他的(E)。
    1. このデータは変更されず、メインメモリと同期されません。
    2. 同じレベルの他のキャッシュには、このデータはありません。
  3. 一般(S)。
    1. このデータは変更および同期されません。
    2. 同じレベルの他のキャッシュも(おそらく)同じデータを持っています。
  4. 無効(I)。
    1. このデータは古いため、使用しないでください。


上記の状態を適用および更新すると、キャッシュの一貫性を実現できます。 4つのコアを持つプロセッサのいくつかの例を見てみましょう。各コアには、独自のL1キャッシュと、チップ上のグローバルL2キャッシュがあります。



メモリエントリ



core-1のスレッドが0xabcdのメモリに書き込みたいと仮定します。 以下は、イベントの可能なシーケンスです。



キャッシュヒット



  1. L1-1には、状態EまたはMのデータがあります。
  2. L1-1は録音中です。 すべて準備完了です。

    1. 他のキャッシュにはデータがないため、すぐに書き込みをしても安全です。
    2. キャッシュラインのステータスは、変更されるとMに変わります。


ローカルキャッシュミス、単一レベルキャッシュヒット



  1. L1-1には状態Sのデータがあります。

    1. これは、別の単一レベルのキャッシュでこのデータを使用できることを意味します。
    2. L1-1にこのデータがまったくない場合、同じシーケンスが適用されます。
  2. L1-1は所有権要求をL2キャッシュに送信します。
  3. L2はそのディレクトリを調べ、L1-2がこのデータを状態Sに持っていることを確認します。
  4. L2はL1-2にsnoop-invalidateを送信します。
  5. L1-2はデータを無効(I)としてマークします。
  6. L1-2はL2にAck要求を送信します。
  7. L2は、Ackと最新のデータをL1-1に送信します。

    1. L2は、L1-1でこのデータが状態Eに格納されていることを確認します。
  8. L1-1には最新のデータと、状態Eに入る許可があります。
  9. L1-1は、このデータの状態を記録してMに変更します。


読書メモリ



ここで、core-2のスレッドがアドレス0xabcdから読み取りたいとします。 以下は、イベントの可能なシーケンスです。



キャッシュヒット



  1. L1-2には、状態S、E、またはMのデータがあります。
  2. L1-2はデータを読み取り、ストリームに戻ります。 できた


ローカルキャッシュミス、トップレベルキャッシュミス



  1. L1-2には状態I(無効)のデータがあります。つまり、使用できません。
  2. L1-2は、Share-for-Share要求をL2キャッシュに送信します。
  3. L2にもデータはありません。 メモリからデータを読み取ります。
  4. L2はメモリからデータを返します。
  5. L2は、状態Sに入る許可とともにL1-2にデータを送信します。

    1. L2は、L1-2でこのデータが状態Sに格納されていることを確認します。
  6. L1-2はデータを受信し、キャッシュに保存して、ストリームに送信します。


ローカルキャッシュミス、トップレベルキャッシュのヒット



  1. L1-2には、状態Iのデータがあります。
  2. L1-2は、L2キャッシュにRequest-for-S要求を送信します。
  3. L2は、L1-1でデータが状態Sにあることを確認します。
  4. L2は、状態Sに入るためのデータと許可とともに、AckをL1-2に送信します。
  5. L1-2はデータを受信し、キャッシュに保存して、ストリームに送信します。


ローカルキャッシュミス、単一レベルキャッシュヒット



  1. L1-2には、状態Iのデータがあります。
  2. L1-2は、L2キャッシュにRequest-for-S要求を送信します。
  3. L2は、L1-1でデータが状態E(またはM)にあることを確認します。
  4. L2はL1-1にsnoop-shareを送信します
  5. L1-1はSに下がります。
  6. 該当する場合、L1-1は変更されたデータとともにAckをL2に送信します。
  7. L2は、データと状態Sに入る許可とともに、AckをL1-2に送信します。
  8. L1-2はデータを受信し、キャッシュに保存して、ストリームに送信します。


バリエーション



上記は可能なシナリオのほんの一部です。 実際、多くのバリエーションがあり、プロトコルの2つの同一の実装はありません。 たとえば、 一部の設計ではO / F状態が使用されますライトバックキャッシュを持つものもあれば、 ライトスルーを使用するものもあります 。 スヌープブロードキャストを使用するものもあれば、スヌープフィルターを使用するものもあります 。 いくつかの包括的キャッシュ、および他の-排他的キャッシュ 。 バリエーションは無限であり、ストアバッファについても触れていません!



さらに、上記の例では、2レベルのキャッシングのみを備えた単純なプロセッサーを検討しています。 ただし、同じプロトコルを再帰的に適用できることに注意してください。 L3キャッシュは簡単に追加でき、上記と同じプロトコルを使用して複数のL2キャッシュを調整します。 完全に異なるチップ上でいくつかのL3キャッシュの動作を調整する「ホームエージェント」を備えたマルチプロセッサシステムがあります。



各シナリオでは、各キャッシュは、最上位キャッシュ(データ/アクセス許可の受信用)およびその子孫(データ/アクセス許可の付与/取り消し用)とのみ対話する必要があります。 これはすべて、プログラムストリームに対して目に見えない形で発生します。 ソフトウェアの観点から見ると、メモリサブシステムは、遅延が非常に変化する単一の一貫したモノリスのように見えます。



同期が依然として重要である理由



コンピューターのメモリシステムの驚くべきパワーと一貫性について説明しました。 1つの質問が残っています。キャッシュの一貫性が非常に高い場合、なぜJavaなどの言語で揮発性があるの



これは非常に複雑な質問であり、 他の場所で最もよく答えられます 。 ちょっとしたヒントをさせてください。 CPUレジスタのデータは、キャッシュ/メモリのデータと同期していません 。 ソフトウェアコンパイラは、データをレジスタロードし、キャッシュに書き戻し 、さらに命令を並べ替える際に、あらゆる種類の最適化を実行します。 これはすべて、コードが1つのスレッドで実行されるという条件の下で行われます。 したがって、競合状態の危険にさらされているデータは、アトミックや揮発性などの並列アルゴリズムと言語構造を使用して手動で保護する必要があります。



Javaのvolatile修飾子の場合、解決策の一部は、すべての読み取り/書き込み操作でローカルレジスタをバイパスし、代わりにすぐに読み取り/書き込みキャッシュにアクセスすることです。 データがL1キャッシュに読み書きされるとすぐに、ハードウェアネゴシエーションプロトコルが有効になります。 すべてのグローバルフローで一貫性を保証します。 したがって、複数のスレッドが1つの変数の読み取り/書き込みを行う場合、それらはすべて互いに同期されます。 これにより、わずか1ナノ秒でスレッド間の調整が行われます。



All Articles