挥发物贵吗?


111

在阅读了有关volatile的实现的JSR-133编译器厨师手册之后,尤其是“与原子指令的交互”部分,我认为读取volatile变量而不更新它需要LoadLoad或LoadStore屏障。在页面的下方,我看到在X86 CPU上,LoadLoad和LoadStore实际上是无操作的。这是否意味着无需在x86上显式地使缓存无效就可以进行volatile读操作,并且它与普通变量读取一样快(不考虑volatile的重新排序约束)?

我相信我无法正确理解。有人可以启发我吗?

编辑:我想知道在多处理器环境中是否存在差异。如John V.所述,在单CPU系统上,CPU可能会查看其自己的线程缓存,但是在多CPU系统上,必须为CPU提供一些配置选项,这还不够,必须命中主内存,从而使volatile速度变慢在多CP​​U系统上,对吗?

PS:在学习更多相关信息的过程中,我偶然发现了以下精彩文章,由于这个问题可能会让其他人感兴趣,因此我将在这里分享我的链接:


1
您可以阅读我有关多个CPU的配置的编辑。可能会发生,在多CPU系统上,如果存在短暂的引用,则只会对主内存进行一次读/写操作。
约翰·温特

2
volatile读取本身并不昂贵。主要成本是它如何阻止优化。实际上,除非在紧密循环中使用了volatile,否则平均成本也不是很高。
无可争议的2011年

2
本文相关的(infoq.com/articles/memory_barriers_jvm_concurrency)也你可能感兴趣的,它显示的挥发性和不同体系结构生成的代码同步的效果。在这种情况下,jvm可以比提前编译的性能更好,因为它知道jvm是否在单处理器系统上运行并且可以省略一些内存障碍。
约恩·霍斯特曼

Answers:


123

在Intel上,无与伦比的volatile读取非常便宜。如果我们考虑以下简单情况:

public static long l;

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

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

利用Java 7的打印汇编代码的能力,运行方法类似于:

# {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个引用,第一个涉及内存的加载,第二个跳过该加载,因为该值已从已加载到的寄存器中被重用(长为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的两个静态引用都涉及来自内存的加载,即,该值不能跨多个易失性读取保存在寄存器中。为了确保有原子读取,将值从主存储器读取到MMX寄存器中movsd 0x6fb7b2f0(%ebp),%xmm0从而使读取操作成为一条指令(从上一个示例中,我们看到在32位系统上64位值通常需要进行两次32位读取)。

因此,易失性读取的总成本将大致相当于存储器负载,并且可以与L1缓存访问一样便宜。但是,如果另一个内核正在写入volatile变量,则高速缓存行将失效,从而需要主存储器或L3高速缓存访​​问权限。实际成本将在很大程度上取决于CPU架构。即使在Intel和AMD之间,缓存一致性协议也有所不同。


侧面说明,Java 6中有显示组件相同的能力(它是做它的热点)
bestsss

+1在JDK5中,对于任何读/写都不能对volatile进行重新排序(例如,它可以修复双重检查锁定)。这是否意味着它也会影响非易失性字段的操作方式?混合使用对易失性和非易失性字段的访问将很有趣。
ewernli 2012年

@evemli,您需要小心,我自己做了一次此声明,但被发现是不正确的。有一个边缘情况。当可以在易失性存储之前对存储进行重新排序时,Java内存模型允许漫游汽车旅馆的语义。如果您是从IBM网站上的Brian Goetz文章中挑选出来的,那么值得一提的是,这篇文章过分简化了JMM规范。
Michael Barker 2012年

20

一般而言,在大多数现代处理器上,易失性负载可与正常负载相比。易失性存储大约是进入/退出监视的时间的1/3。这在高速缓存一致的系统上可以看到。

为了回答OP的问题,易失性写入非常昂贵,而读取则通常不是。

这是否意味着无需在x86上显式地使缓存无效就可以执行volatile读操作,并且它与普通变量读取一样快(不考虑volatile的重新排序约束)?

是的,有时在验证字段时,CPU甚至可能没有命中主内存,而是监视其他线程缓存并从那里获取值(非常笼统的解释)。

但是,我赞同尼尔的建议,如果您有一个由多个线程访问的字段,您可以将其包装为AtomicReference。作为AtomicReference,它执行的读写吞吐量大致相同,但更明显的是,该字段将由多个线程访问和修改。

编辑以回答OP的编辑:

缓存一致性有点复杂,但是简而言之:CPU将共享一条附加到主内存的通用缓存行。如果一个CPU加载了内存而没有其他CPU拥有该内存,则该CPU将假定它是最新的值。如果另一个CPU尝试加载相同的内存位置,则已经加载的CPU会意识到这一点,并实际上与请求CPU共享缓存的引用-现在,请求CPU在其CPU缓存中具有该内存的副本。(它不必在主存储器中寻找参考)

协议涉及的内容更多,但这可以让您了解正在发生的事情。还要回答您的另一个问题,在没有多个处理器的情况下,易失性读/写实际上可能比使用多个处理器快。实际上,有些应用程序在单个CPU然后多个CPU上并发运行的速度更快。


5
AtomicReference只是对volatile字段的包装,其中添加了本机函数,提供了诸如getAndSet,compareAndSet等其他功能,因此从性能的角度来看,如果需要添加的功能,则使用它是有用的。但是我不知道为什么在这里提到操作系统?该功能直接在CPU操作码中实现。并且这是否意味着在多处理器系统上,一个CPU不了解其他CPU的缓存内容,因此由于CPU总是需要访问主内存,因此易失性变慢了吗?
丹尼尔(Daniel)

您是对的,我想念有关OS应该写CPU的问题,现在解决了。是的,我确实知道AtomicReference只是易失字段的包装器,但它也作为一种文档添加了该字段本身将由多个线程访问。
约翰·温特

@John,为什么还要通过AtomicReference添加另一个间接寻址?如果您需要CAS-好的,但是AtomicUpdater可能是一个更好的选择。据我记得,没有关于AtomicReference的内在函数。
bestsss 2011年

@bestsss对于所有一般用途,AtomicReference.set / get和volatile加载和存储之间没有区别。话虽这么说,我对何时使用它有相同的感觉(并做了一定程度的尝试)。此响应可以使它详细一点stackoverflow.com/questions/3964317/…。使用这两者都是一种偏爱,我对简单的volatile使用AtomicReference的唯一论据是清晰的文档-据我所知,它本身并没有产生最大的论据
John Vint

附带一提,有人认为使用volatile字段/ AtomicReference(无需CAS)会导致代码有问题。old.nabble.com/…
John Vint

12

用Java内存模型(在JSR 133中为Java 5+定义)的话来说,对volatile变量的任何操作(读或写)都会在发生之前相对于对该变量的任何其他操作关系。这意味着编译器和JIT被迫避免某些优化,例如对线程内的指令重新排序或仅在本地缓存内执行操作。

由于无法进行某些优化,因此生成的代码必然比原本要慢,尽管可能不会非常多。

但是,volatile除非您知道将要从synchronized块之外的多个线程访问该变量,否则不应创建变量。即使这样,您也应该考虑volatile是否是的最佳选择synchronizedAtomicReference以及,它的朋友,显式Lock类等。


4

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

更一般地说,在多处理器系统上,我看不到如何不影响变量地访问volatile变量-必须有某种方法来确保对处理器A的写入将与对处理器B的读取同步。


4
关于指令的重排序可能性,读易失性变量与监视输入有相同的代价,而写易失性变量则等于监视退出。不同之处可能是哪些变量(例如处理器缓存)被刷新或无效。当同步刷新或使所有内容无效时,对volatile变量的访问应始终忽略高速缓存。
丹尼尔(Daniel)

12
-1,访问易失性变量与使用同步块有很大不同。输入同步块需要基于原子的compareAndSet的写操作来取出锁,并使用易失的写操作来释放锁。如果锁满足,则控制权必须从用户空间传递到内核空间以仲裁锁(这是昂贵的位)。访问volatile将始终留在用户空间中。
Michael Barker

@MichaelBarker:您确定所有监视器都必须由内核而不是应用程序保护吗?
丹尼尔(Daniel)

@Daniel:如果您使用同步块或锁定来表示监视器,则可以,但前提是该监视器满足条件。无需内核仲裁即可做到这一点的唯一方法是使用相同的逻辑,但是忙于自旋而不是驻留线程。
Michael Barker

@MichaelBarker:Okey,对于满意的锁,我理解这一点。
丹尼尔(Daniel)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.