web-dev-qa-db-ja.com

揮発性は高価ですか?

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

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

編集:マルチプロセッサ環境に違いがあるのだろうか。シングルCPUシステムでは、ジョンVが述べているように、CPUはそれ自身のスレッドキャッシュを見るかもしれませんが、マルチCPUシステムでは、これが十分ではなく、メインメモリをヒットしなければならないCPUの設定オプションが必要です。マルチCPUシステムでは正しいですか?

PS:これについてさらに学ぶために、次のすばらしい記事につまずきました。この質問は他の人にとって興味深いかもしれないので、ここでリンクを共有します。

104
Daniel

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つの参照を見ると、最初はメモリからのロードを含み、2番目は既にロードされているレジスタから値が再利用されるためロードをスキップします(64ビットと32ビットのラップトップでは2つのレジスタを使用します)。

L変数をvolatileにすると、結果のAssemblyは異なります。

_# {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ビット値は通常2つの32ビット読み取り32ビットシステム)。

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

116
Michael Barker

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

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

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

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

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

編集してOPの編集に答えます:

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

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

20
John Vint

Java Memory Model(JSR 133でJava 5+に対して定義されている))の言葉では、すべての操作-読み取りまたは書き込み-volatile変数は、同じ変数に対する他の操作に関してhappens-before関係を作成します。これは、コンパイラーとJITがスレッド内の命令の並べ替えなどの特定の最適化を回避することを強制されることを意味しますまたは、ローカルキャッシュ内でのみ操作を実行します。

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

それにもかかわらず、volatileブロック外の複数のスレッドからアクセスされることがわかっている場合を除き、変数synchronizedを作成しないでください。その場合でも、synchronizedAtomicReferenceおよびそのフレンド、明示的なLockクラスなどに対して、volatileが最良の選択であるかどうかを検討する必要があります。

11
Neil Bartlett

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

より一般的には、マルチプロセッサシステムでは、揮発性変数へのアクセスがペナルティなしでどのように行われるかわかりません。

4
krakover