揮発性は高価ですか?


111

揮発性の実装、特にセクション「アトミック命令との相互作用」について、コンパイラライター向けのJSR-133クックブックを読んだ後、更新せずに揮発性変数を読み取るには、LoadLoadまたはLoadStoreバリアが必要だと思います。ページのさらに下を見ると、LoadLoadとLoadStoreは、X86 CPUでは実質的に何もしません。これは、x86で明示的なキャッシュ無効化なしで揮発性読み取り操作を実行でき、通常の変数読み取りと同じくらい高速であることを意味しますか(揮発性の並べ替えの制約を無視)。

私はこれを正しく理解していないと思います。誰かが私を啓蒙したいと思いませんか?

編集:マルチプロセッサ環境に違いがあるのでしょうか。ジョンVが述べているように、シングルCPUシステムでは、CPUはそれ自体のスレッドキャッシュを調べる可能性がありますが、マルチCPUシステムでは、CPUにいくつかの構成オプションが必要です。マルチCPUシステムで、そうですか?

PS:これについて詳しく知る途中で、次のすばらしい記事に出くわしました。この質問は他の人にとって興味深いかもしれないので、ここでリンクを共有します。


1
あなたが参照している複数のCPUを使った構成に関する私の編集を読むことができます。短命の参照のマルチCPUシステムでは、メインメモリへの単一の読み取り/書き込みが行われることはありません。
John Vint、2011年

2
揮発性読み取り自体は高価ではありません。主なコストは、最適化を妨げる方法です。実際には、タイトなループでvolatileが使用されていない限り、平均コストもそれほど高くありません。
評判の悪い2011年

2
infoqに関するこの記事(infoq.com/articles/memory_barriers_jvm_concurrency)も興味深いかもしれません。さまざまなアーキテクチャーで生成されたコードでのvolatileとSynchronizedの影響を示しています。これは、jvmがユニプロセッサーシステムで実行されているかどうかを認識し、いくつかのメモリバリアを省略できるため、jvmが事前コンパイラよりも優れたパフォーマンスを発揮できる1つのケースでもあります。
ジョーンHorstmann

回答:


123

Intelでは、競合しない揮発性読み取りは非常に安価です。次の単純なケースを考えると:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

アセンブリコードを出力するJava 7の機能を使用すると、runメソッドは次のようになります。

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

getstaticへの2つの参照を見ると、1つ目はメモリからのロードに関係し、2つ目は既にロードされているレジスタから値が再利用されるため、ロードをスキップします(longは64ビットで、32ビットのラップトップで) 2つのレジスタを使用します)。

l変数を揮発性にすると、結果のアセンブリは異なります。

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

この場合、変数lへのgetstatic参照は両方ともメモリからのロードを含みます。つまり、値は複数の揮発性読み取りにわたってレジスタに保持できません。アトミックな読み取りを確実に行うために、値はメインメモリからMMXレジスタmovsd 0x6fb7b2f0(%ebp),%xmm0に読み込まれ、読み取り操作が単一の命令になります(前の例から、64ビットの値では通常、32ビットシステムで32ビットの読み取りが2回必要になることがわかりました)。

したがって、揮発性読み取りの全体的なコストは、メモリの負荷とほぼ同等であり、L1キャッシュアクセスと同じくらい安価です。ただし、別のコアがvolatile変数に書き込んでいる場合、メインメモリまたはおそらくL3キャッシュアクセスを必要とするキャッシュラインが無効になります。実際のコストは、CPUアーキテクチャに大きく依存します。IntelとAMDの間でも、キャッシュコヒーレンシプロトコルは異なります。


補足として、Java 6にはアセンブリを表示する同じ機能があります(それを行うのはホットスポットです)
bestsss

JDK5揮発性缶に+1に対して並べ替えされない任意の(例えば、二重チェックロックを固定)読み取り/書き込み。それは、不揮発性フィールドの操作方法にも影響することを意味しますか?揮発性フィールドと不揮発性フィールドへのアクセスを混在させることは興味深いでしょう。
ewernli 2012年

@evemli、あなたは注意する必要があります、私はこの声明を一度自分で作りましたが、間違っていることがわかりました。エッジケースあり。揮発性ストアの前にストアを再注文できる場合、Javaメモリモデルを使用すると、ローチモーテルのセマンティクスが可能になります。IBMサイトのBrian Goetzの記事からこれを取り上げた場合、この記事がJMM仕様を単純化しすぎていることは言及する価値があります。
Michael Barker

20

一般的に言えば、ほとんどの最新のプロセッサでは、揮発性負荷は通常の負荷と同等です。揮発性ストアは、monitor-enter / monitor-exitの約1/3の時間です。これは、キャッシュコヒーレントなシステムで見られます。

OPの質問に答えるために、揮発性書き込みは高価ですが、読み取りは通常は高価ではありません。

これは、x86で明示的なキャッシュ無効化なしで揮発性読み取り操作を実行でき、通常の変数読み取りと同じくらい高速であることを意味しますか(揮発性の並べ替えの制約を無視)。

はい、場合によっては、フィールドを検証するときに、CPUがメインメモリにヒットしないこともあり、代わりに他のスレッドキャッシュをスパイしてそこから値を取得します(非常に一般的な説明)。

ただし、複数のスレッドからアクセスされるフィールドがある場合は、そのフィールドをAtomicReferenceとしてラップするというニールの提案を2番目に引用します。AtomicReferenceであるため、読み取り/書き込みの場合とほぼ同じスループットを実行しますが、フィールドが複数のスレッドによってアクセスおよび変更されることも明白です。

OPの編集に答えるために編集します。

キャッシュコヒーレンスは少し複雑なプロトコルですが、要するに、CPUはメインメモリに接続されている共通のキャッシュラインを共有します。CPUがメモリをロードし、他のCPUにメモリがない場合、CPUはそれが最新の値であると想定します。別のCPUが同じメモリロケーションをロードしようとすると、すでにロードされているCPUはこれを認識し、実際にリクエストしたCPUへのキャッシュされた参照を共有します。これで、リクエストCPUはそのメモリのコピーをCPUキャッシュに持っています。(参照のためにメインメモリを調べる必要はありませんでした)

かなり多くのプロトコルが含まれていますが、これにより、何が起こっているのかがわかります。また、他の質問に答えるために、複数のプロセッサーがないため、揮発性の読み取り/書き込みは、実際には複数のプロセッサーよりも高速になる可能性があります。実際には、単一のCPUで複数のアプリケーションを同時に実行すると、より高速に実行されるアプリケーションがいくつかあります。


5
AtomicReferenceは、getAndSet、compareAndSetなどの追加機能を提供する追加のネイティブ関数を含むvolatileフィールドのラッパーにすぎないため、パフォーマンスの観点からは、追加機能が必要な場合に使用すると便利です。しかし、ここでOSを参照する理由は何でしょうか。この機能は、CPUオペコードに直接実装されています。そして、これは、CPUが常にメインメモリにアクセスする必要があるため、1つのCPUが他のCPUのキャッシュの内容を認識しておらず、揮発性が遅いということを意味しますか?
Daniel

OSがCPUを作成する必要があると私が話していなかったので、それを修正しています。そして、はい、AtomicReferenceが単に揮発性フィールドのラッパーであることは知っていますが、フィールド自体が複数のスレッドによってアクセスされることを一種のドキュメントとして追加します。
John Vint

@John、なぜAtomicReferenceを介して別の間接参照を追加するのですか?CASが必要な場合-わかりましたが、AtomicUpdaterの方が適している可能性があります。私が覚えている限り、AtomicReferenceに関する組み込み関数はありません。
bestsss

@bestsssすべての一般的な目的で、AtomicReference.set / getと揮発性ロードおよびストアの間に違いはありません。そうは言っても、いつ使用するかについては、私は同じ気持ちで(ある程度までは)感じました。この応答は、それを少し詳しく説明します。stackoverflow.com/questions/3964317/…。どちらかを使用する方が好みです。単純なvolatileではなくAtomicReferenceを使用するための私の唯一の引数は、明確なドキュメンテーションのためです-それ自体が私が理解する最大の引数にならない
John Vint

サイドノートでは、いくつかのバグだらけのコードへのリード線(CASを必要とせずに)揮発性フィールド/ AtomicReferenceを使用して主張old.nabble.com/...
ジョンVintの

12

Javaメモリモデル(JSR 133でJava 5+に対して定義されている)の言葉では、volatile変数に対するすべての操作(読み取りまたは書き込み)は、同じ変数に対する他のすべての操作に対して「前に発生」関係を作成します。つまり、コンパイラーとJITは、スレッド内での命令の並べ替えやローカルキャッシュ内でのみ操作を実行するなど、特定の最適化を回避する必要があります。

一部の最適化が利用できないため、結果として得られるコードは、おそらくそれほどではないにしても、必然的に遅くなります。

それでもvolatilesynchronizedブロック外の複数のスレッドからアクセスされることがわかっている場合を除き、変数を作成しないでください。それでもあなたは揮発性が対最良の選択であるかどうかを検討すべきでsynchronizedAtomicReferenceその友人、明示的およびLockその他のクラス、


4

揮発性変数へのアクセスは、多くの点で、同期ブロック内の通常の変数へのアクセスをラップするのと似ています。たとえば、揮発性変数へのアクセスは、CPUがアクセスの前後に命令を並べ替えることを防ぎ、これは一般に実行速度を低下させます(ただし、どれほどかはわかりません)。

より一般的には、マルチプロセッサシステムでは、揮発性変数へのアクセスがペナルティなしにどのように行われるかはわかりません。プロセッサAでの書き込みをプロセッサBでの読み取りに確実に同期させる方法が必要です。


4
揮発性変数を書き込むことはモニター出口と同じであるのに対し、揮発性変数を読み取ることは、命令の並べ替えの可能性に関して、モニター入力を行うことと同じペナルティを持っています。違いは、どの変数(プロセッサキャッシュなど)がフラッシュまたは無効化されるかです。同期するとすべてがフラッシュまたは無効化されますが、揮発性変数へのアクセスは常にキャッシュを無視する必要があります。
Daniel

12
-1、揮発性変数へのアクセスは、同期ブロックの使用とはかなり異なります。同期ブロックに入るには、ロックを解除するためのアトミックcompareAndSetベースの書き込みと、ロックを解放するための揮発性書き込みが必要です。ロックが競合している場合、制御をユーザー空間からカーネル空間に渡して、ロックを調停する必要があります(これは高価なビットです)。volatileへのアクセスは常にユーザー空間にとどまります。
Michael Barker

@MichaelBarker:すべてのモニターをアプリではなくカーネルで保護する必要がありますか?
ダニエル

@Daniel:同期ブロックまたはロックを使用してモニターを表す場合、はい、ただしモニターにコンテンツがある場合のみ。カーネルアービトレーションなしでこれを行う唯一の方法は、同じロジックを使用することですが、スレッドを停止する代わりにビジースピンを使用します。
Michael Barker

@MichaelBarker:オーケー、内容のあるロックについては、私はこれを理解しています。
Daniel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.