揮発性物質は高価ですか? 質問する

揮発性物質は高価ですか? 質問する

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

私はこれを正しく理解していないと思います。どなたか教えていただけませんか?

編集: マルチプロセッサ環境では違いがあるのだろうか。シングル CPU システムでは、John V. が述べているように、CPU は自身のスレッド キャッシュを参照するかもしれませんが、マルチ CPU システムでは、それだけでは不十分でメイン メモリにアクセスする必要があり、マルチ CPU システムでは volatile が遅くなるという CPU の設定オプションがあるはずですよね?

追記: この件についてさらに詳しく調べている途中で、偶然以下の素晴らしい記事を見つけました。この質問は他の方にも興味深いかもしれないので、ここでリンクを共有します。

ベストアンサー1

Intel では、競合のない volatile 読み取りは非常に安価です。次の単純なケースを考えてみましょう。

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

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

# {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読み取り操作が 1 つの命令になります (前の例から、64 ビット値は通常、32 ビット システムで 2 つの 32 ビット読み取りを必要とすることがわかりました)。

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

おすすめ記事