web-dev-qa-db-ja.com

Java 8安全ではない:xxxFence()命令

Java 8では、3つのメモリバリア命令がUnsafeクラスに追加されました( ソース ):

_/**
 * Ensures lack of reordering of loads before the fence
 * with loads or stores after the fence.
 */
void loadFence();

/**
 * Ensures lack of reordering of stores before the fence
 * with loads or stores after the fence.
 */
void storeFence();

/**
 * Ensures lack of reordering of loads or stores before the fence
 * with loads or stores after the fence.
 */
void fullFence();
_

次の方法でメモリバリアを定義すると(多かれ少なかれ理解しやすいと思います):

XとYを、並べ替えの対象となる操作タイプ/クラスと見なします。

X_YFence()は、バリアが開始された後のタイプYの操作の前に、バリアの前のタイプXのすべての操作が完了することを保証するメモリバリア命令です。

これで、バリア名をUnsafeから次の用語に「マッピング」できます。

  • loadFence()load_loadstoreFence()になります。
  • storeFence()store_loadStoreFence()になります。
  • fullFence()loadstore_loadstoreFence()になります。

最後に、私の質問は-なぜload_storeFence()store_loadFence()store_storeFence()およびload_loadFence()

私の推測では、それらは本当に必要なものではありませんが、現時点ではその理由がわかりません。そこで、追加しない理由を知りたいのですが。それについての推測も歓迎します(ただし、これによってこの質問が意見に基づいたものとして話題から外れないことを願っています)。

前もって感謝します。

45
Alexey Malev

概要

CPUコアには、アウトオブオーダー実行を支援するための特別なメモリオーダリングバッファがあります。これらは、ロードとストアで別々にすることができます(通常は別々です)。ロード順序バッファー用のLOBとストア順序バッファー用のSOBです。

Unsafe API用に選択されたフェンシング操作は、次の仮定に基づいて選択されています。基盤となるプロセッサには個別のロード順序バッファがあります(ロードの並べ替え用)。ストア順序バッファー(ストアの並べ替え用)。

したがって、この仮定に基づいて、ソフトウェアの観点から、CPUに次の3つのいずれかを要求できます。

  1. LOBを空にする(loadFence):LOBがすべてのエントリを処理するまで、このコアで他の命令の実行が開始されないことを意味します。 x86では、これはLFENCEです。
  2. SOBを空にする(storeFence):SOBのすべてのエントリが処理されるまで、他の命令がこのコアで実行を開始しないことを意味します。 x86では、これはSFENCEです。
  3. LOBとSOBの両方を空にする(fullFence):上記の両方を意味します。 x86では、これはMFENCEです。

実際には、特定のプロセッサアーキテクチャごとに異なるメモリ順序の保証が提供されます。これは、上記よりも厳格または柔軟な場合があります。たとえば、SPARCアーキテクチャはロードストアとストアロードのシーケンスを並べ替えることができますが、x86はそれを行いません。さらに、LOBとSOBを個別に制御できない(つまり、フルフェンスのみ)アーキテクチャが存在します。ただし、どちらの場合も次のようになります。

  • アーキテクチャがより柔軟な場合、APIは、選択の問題として、「緩い」シーケンスの組み合わせへのアクセスを提供しません。

  • アーキテクチャがより厳格な場合、APIはすべての場合に、より厳格なシーケンス保証を実装するだけです(たとえば、3つの呼び出しすべてが実際に実行され、完全なフェンスとして実装されます)

特定のAPIを選択する理由は、assyliasが提供する100%オンザスポットの回答に従ってJEPで説明されています。メモリオーダリングとキャッシュコヒーレンスについて知っている場合は、assyliasの答えで十分です。それらがC++ APIの標準化された命令と一致するという事実が主要な要因だったと思います(JVMの実装を大幅に簡素化します): http://en.cppreference.com/w/cpp/atomic/memory_order おそらく、実際の実装では、特別な命令を使用する代わりに、それぞれのC++ APIが呼び出されます。

以下に、x86ベースの例を使用した詳細な説明があります。これにより、これらのことを理解するために必要なすべてのコンテキストが提供されます。実際、境界が定められている(以下のセクションは別の質問に答えます:「x86アーキテクチャでキャッシュコヒーレンスを制御するためにメモリフェンスがどのように機能するかの基本的な例を提供できますか?」

これは、x86でキャッシュコヒーレンスが実際にどのように機能するかの具体例を学ぶまで、私自身(ハードウェア設計者ではなくソフトウェア開発者)がメモリオーダリングとは何かを理解するのに苦労したためです。これは、一般的なメモリフェンスについて議論するための貴重なコンテキストを提供します(他のアーキテクチャについても同様です)。最後に、x86の例から得られた知識を使用してSPARC

参考資料[1]はさらに詳細な説明であり、x86、SPARC、ARM、およびPowerPC)のそれぞれについて説明するための個別のセクションがあるため、詳細に興味がある場合は優れた読み物です。詳細。


x86アーキテクチャの例

x86は、LFENCE(ロードフェンス)、SFENCE(ストアフェンス)、MFENCE(ロードストアフェンス)の3種類のフェンシング命令を提供するため、100%をJava APIにマップします。

これは、x86には個別のロード順バッファー(LOB)とストア順バッファー(SOB)があるため、実際にはLFENCE/SFENCE命令がそれぞれのバッファーに適用されるのに対し、MFENCEは両方に適用されます。

SOBは、(プロセッサからキャッシュシステムへの)発信値を格納するために使用され、キャッシュコヒーレンスプロトコルは、キャッシュラインへの書き込み許可を取得するために機能します。 LOBは、無効化が非同期で実行できるように無効化要求を格納するために使用されます(そこで実行されるコードが実際にその値を必要としないことを期待して、受信側でのストールを減らします)。

故障した店舗とSFENCE

以下のルーチンを実行する2つのCPU、0と1を備えたデュアルプロセッサシステムがあるとします。 failureを保持するキャッシュラインが最初にCPU1によって所有されているのに対し、shutdownを保持するキャッシュラインが最初にCPU0によって所有されている場合を考えてみます。

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  if (failure) { ...}
}

ストアフェンスがない場合、CPU 0は障害によるシャットダウンを通知する場合がありますが、CPU 1はループを終了し、ifブロックで障害処理に参加しません。

これは、CPU0がfailureの値1をストアオーダーバッファに書き込み、キャッシュコヒーレンスメッセージを送信してキャッシュラインへの排他的アクセスを取得するためです。次に、(排他的アクセスを待機している間)次の命令に進み、すぐにshutdownフラグを更新します(このキャッシュラインはすでにCPU0によって排他的に所有されているため、他のコアとネゴシエートする必要はありません)。最後に、後でCPU1から無効化確認メッセージ(failureに関して)を受信すると、failureのSOBの処理に進み、値をキャッシュに書き込みます(ただし、順序は逆になります)。 )。

StoreFence()を挿入すると、次の問題が修正されます。

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  SFENCE // next instruction will execute after all SOBs are processed
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  if (failure) { ...}
}

言及に値する最後の側面は、x86にストア転送があることです。CPUが(キャッシュコヒーレンスのために)SOBでスタックする値を書き込むと、SOBが実行される前に同じアドレスに対してロード命令を実行しようとする場合があります。処理され、キャッシュに配信されます。したがって、CPUはキャッシュにアクセスする前にSOBを参照するため、この場合に取得される値は、SOBから最後に書き込まれた値です。 これは、このコアからのストアは、何があっても、このコアからの後続のロードで並べ替えることができないことを意味します

異常な負荷とLFENCE

ここで、ストアフェンスが設置されていて、CPU1に向かう途中でshutdownfailureを追い越せず、反対側に集中できないことに満足していると仮定します。店のフェンスがある場合でも、間違ったことが起こるシナリオがあります。 failureが両方のキャッシュ(共有)にあるのに対し、shutdownはCPU0のキャッシュにのみ存在し、排他的に所有されている場合を考えてみます。悪いことが次のように発生する可能性があります。

  1. CPU0は1をfailureに書き込みます。 また、キャッシュコヒーレンスプロトコルの一部として、共有キャッシュラインのコピーを無効にするメッセージをCPU1に送信します。
  2. CPU0はSFENCEを実行してストールし、failureに使用されるSOBがコミットするのを待ちます。
  3. CPU1はwhileループのためにshutdownをチェックし、(値が欠落していることに気づき)キャッシュコヒーレンスメッセージを送信して値を読み取ります。
  4. CPU1は、ステップ1でCPU0からfailureを無効にするメッセージを受信し、即座に確認応答を送信します。 注:これは無効化キューを使用して実装されるため、実際には、後で無効化を行うためにメモを入力する(LOBにエントリを割り当てる)だけですが、送信する前に実際に実行することはありません。謝辞。
  5. CPU0はfailureの確認応答を受信し、SFENCEを過ぎて次の命令に進みます
  6. CPU0は、すでにキャッシュラインを排他的に所有しているため、SOBを使用せずにシャットダウンに1を書き込みます。 キャッシュラインはCPU0専用であるため、無効化のための追加メッセージは送信されません
  7. CPU1はshutdown値を受け取り、それをローカルキャッシュにコミットして、次の行に進みます。
  8. CPU1はifステートメントのfailure値をチェックしますが、無効化キュー(LOBノート)はまだ処理されていないため、ローカルキャッシュの値0を使用します(ifブロックには入りません)。
  9. CPU1は無効化キューを処理し、failureを1に更新しますが、すでに手遅れです...

ロードオーダーバッファと呼ばれるものは、実際には無効化要求のキューイングであり、上記は次の方法で修正できます。

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  SFENCE // next instruction will execute after all SOBs are processed
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  LFENCE // next instruction will execute after all LOBs are processed
  if (failure) { ...}
}

X86に関するあなたの質問

SOB/LOBの機能がわかったところで、次の組み合わせについて考えてみましょう。

loadFence() becomes load_loadstoreFence();

いいえ、ロードフェンスはLOBが処理されるのを待機し、基本的に無効化キューを空にします。これは、後続のすべてのロードがキャッシュサブシステム(コヒーレント)からフェッチされるため、最新のデータ(並べ替えなし)を参照することを意味します。ストアはLOBを通過しないため、後続のロードで並べ替えることはできません。 (さらに、ストア転送はローカルで変更されたcachce行を処理します)この特定のコア(ロードフェンスを実行するコア)の観点から、ロードフェンスに続くストアは、すべてのレジスタにデータがロードされた後に実行されます。それを回避する方法はありません。

load_storeFence() becomes ???

Load_storeFenceは意味がないため、必要ありません。何かを保存するには、入力を使用して計算する必要があります。入力をフェッチするには、ロードを実行する必要があります。ストアは、ロードからフェッチされたデータを使用して発生します。ロード時に他のすべてのプロセッサからの最新の値を確認したい場合は、loadFenceを使用してください。フェンスストア転送後のロードの場合、一貫した順序が処理されます。

他のすべてのケースも同様です。


SPARC

SPARCはさらに柔軟性があり、後続のロード(および後続のストアでのロード)でストアを並べ替えることができます。私はSPARCにあまり詳しくなかったので、私の[〜#〜] guess [〜#〜]は、ストア転送がないということでした(SOBはアドレスをリロードするときに参照されない)ので、「ダーティリード」が可能です。実際、私は間違っていました。[3]でSPARCアーキテクチャを見つけましたが、実際には、ストア転送はスレッド化されています。セクション5.3.4から:

すべてのロードは、ストアバッファ(同じスレッドのみ)で読み取り後書き込み(RAW)の危険性をチェックします。完全なRAWは、ロードのdwordアドレスがSTB内のストアのアドレスと一致し、ロードのすべてのバイトがストアバッファーで有効な場合に発生します。部分的なRAWは、dwordアドレスが一致するときに発生しますが、ストアバッファですべてのバイトが有効ではありません。 (たとえば、ST(ワードストア)の後に同じアドレスへのLDX(dwordロード)が続くと、完全なdwordがストアバッファエントリにないため、部分的なRAWになります。)

したがって、異なるスレッドは異なるストア順序バッファを参照するため、ストア後にダーティリードが発生する可能性があります。


参考文献

[1]メモリバリア:ソフトウェアハッカーのハードウェアビュー、Linux Technology Center、IBM Beaverton http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf ==

[2]インテル®64およびIA-32アーキテクチャーソフトウェア開発者マニュアル、第3A巻 http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia- 32-architectures-software-developer-vol-3a-part-1-manual.pdf

[3] OpenSPARC T2コアマイクロアーキテクチャ仕様 http://www.Oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-microarch-1537749.html

58
Alexandros

良い情報源は JEP 171自体 です。

理論的根拠:

3つの方法は、特定のアクセス(ロードとストア)が並べ替えられないようにするために一部のコンパイラーとプロセッサーが必要とする3種類のメモリーフェンスを提供します。

実装(抜粋):

c ++ランタイムバージョン(prims/unsafe.cpp)の場合、既存のOrderAccessメソッドを介して実装します。

    loadFence:  { OrderAccess::acquire(); }
    storeFence: { OrderAccess::release(); }
    fullFence:  { OrderAccess::fence(); }

言い換えると、新しいメソッドは、JVMおよびCPUレベルでのメモリフェンスの実装方法に密接に関連しています。また、ホットスポットが実装されている言語である C++で使用可能なメモリバリア命令 とも一致します。

よりきめ細かいアプローチはおそらく実現可能でしたが、その利点は明らかではありません。

たとえば、 JSR 133 Cookbook のcpu命令の表を見ると、LoadStoreとLoadLoadがほとんどのアーキテクチャで同じ命令にマップされていることがわかります。つまり、どちらも事実上Load_LoadStore命令です。したがって、JVMレベルで単一のLoad_LoadStore(loadFence)命令を使用することは、合理的な設計上の決定のように思われます。

7
assylias

StoreFence()のドキュメントが間違っています。 https://bugs.openjdk.Java.net/browse/JDK-8038978 を参照してください

loadFence()はLoadLoadとLoadStoreであるため、取得フェンスと呼ばれることがよくあります。

storeFence()はStoreStoreとLoadStoreであるため、リリースフェンスと呼ばれることがよくあります。

LoadLoad LoadStore StoreStoreは安価なフェンスです(x86またはSparcではnop、Powerでは安価、ARMではおそらく高価です)。

IA64には、セマンティクスを取得および解放するためのさまざまな命令があります。

fullFence()は、LoadLoad LoadStoreStoreStoreとStoreLoadです。

StordLoadフェンスは(ほぼすべてのCPUで)高価であり、フルフェンスとほぼ同じくらい高価です。

それはAPI設計を正当化します。

4
ntysdd