Is volatile expensive?

After reading The JSR-133 Cookbook for Compiler Writers about the implementation of volatile, especially section "Interactions with Atomic Instructions" I assume that reading a volatile variable without updating it needs a LoadLoad or a LoadStore barrier. Further down the page I see that LoadLoad and LoadStore are effectively no-ops on X86 CPUs. Does this mean that volatile read operations can be done without a explicit cache invalidation on x86, and is as fast as a normal variable read (disregarding the reordering constraints of volatile)?

I believe I don't understand this correctly. Could someone care to enlighten me?

EDIT: I wonder if there are differences in multi-processor environments. On single CPU systems the CPU might look at it's own thread caches, as John V. states, but on multi CPU systems there must be some config option to the CPUs that this is not enough and main memory has to be hit, making volatile slower on multi cpu systems, right?

PS: On my way to learn more about this I stumbled about the following great articles, and since this question may be interesting to others, I'll share my links here:

25298 次浏览

访问 Volatile变量在很多方面类似于在同步块中包装对普通变量的访问。例如,对 Volatile变量的访问会阻止 CPU 在访问之前和之后对指令进行重新排序,这通常会降低执行速度(尽管我不能说降低了多少)。

更一般地说,在多处理器系统上,我不认为访问一个 Volatile变量可以不受惩罚——必须有某种方法来确保处理器 a 上的写操作与处理器 b 上的读操作同步。

用 Java 内存模型(JSR 133中为 Java 5 + 定义的)的话来说,对 volatile变量的任何操作——读或写——相对于对同一变量的任何其他操作都会创建一个 以前发生过关系。这意味着编译器和 JIT 必须避免某些优化,比如在线程中重新排序指令,或者只在本地缓存中执行操作。

由于一些优化是不可用的,所以生成的代码必然比原本的速度要慢,尽管可能不会慢很多。

尽管如此,您不应该创建一个变量 volatile,除非您知道它将从 synchronized块之外的多个线程访问。即使这样,您也应该考虑是否比起 synchronizedAtomicReference和它的朋友、显式的 Lock类来说,易失性是最好的选择。

一般来说,在大多数现代处理器上,易失性负载与正常负载相当。易失性存储大约是 monitor-enter/monitor-exit 时间的1/3。这可以在缓存相关的系统上看到。

要回答 OP 的问题,可变写操作开销很大,而读操作通常开销不大。

Does this mean that volatile read 操作可以在没有 显式缓存失效, 和普通变量读取一样快 (无视重新排序 挥发性约束) ?

是的,有时在验证一个字段时,CPU 甚至可能不会触及主内存,而是监视其他线程缓存并从中获取值(非常一般的解释)。

然而,我支持 Neil 的建议,即如果有一个字段被多个线程访问,那么应该将其包装为 AtomicReference。作为 AtomicReference,它对读/写执行大致相同的吞吐量,但是更明显的是,该字段将被多个线程访问和修改。

编辑回答 OP 的编辑:

缓存一致性是一个有点复杂的协议,但简而言之: CPU 将共享一个公共的缓存线路,连接到主存。如果一个 CPU 加载内存,而没有其他 CPU 拥有它,那么 CPU 将假定它是最新的值。如果另一个 CPU 试图加载相同的内存位置,已经加载的 CPU 会意识到这一点,并实际上共享缓存引用请求的 CPU-现在请求 CPU 有一个副本的内存在其 CPU 缓存。(它从来不需要在主存中查找引用)

有相当多的协议涉及,但这给出了一个什么是正在发生的想法。另外,为了回答您的其他问题,在没有多个处理器的情况下,易失性读/写实际上比使用多个处理器更快。有些应用程序实际上可以在单个 CPU 上并发运行,而不是在多个 CPU 上并发运行。

On Intel an un-contended volatile read is quite cheap. If we consider the following simple case:

public static long l;


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


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

使用 Java7打印汇编代码的能力 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

If you look at the 2 references to getstatic, the first involves a load from memory, the second skips the load as the value is reused from the register(s) it is already loaded into (long is 64 bit and on my 32 bit laptop it uses 2 registers).

如果我们使 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

In this case both of the getstatic references to the variable l involves a load from memory, i.e. the value can not be kept in a register across multiple volatile reads. To ensure that there is an atomic read the value is read from main memory into an MMX register movsd 0x6fb7b2f0(%ebp),%xmm0 making the read operation a single instruction (from the previous example we saw that 64bit value would normally require two 32bit reads on a 32bit system).

因此,易失性读取的总体成本大致相当于内存负载,可能与 L1缓存访问一样便宜。然而,如果另一个内核正在写入 Volatile变量,那么缓存线路将会失效,因为需要一个主存或者可能是一个 l3缓存访问。实际成本将在很大程度上取决于 CPU 体系结构。即使在 Intel 和 AMD 之间,缓存一致性协议也是不同的。