是不稳定的昂贵?

阅读JSR-133编译器编写者手册,了解易失性的实现,尤其是“与原子指令的交互”部分我假设读取一个易失性变量而不更新它需要一个LoadLoad或一个LoadStore屏障。 在页面的下方,我看到LoadLoad和LoadStore在X86 CPU上是无效的。 这是否意味着可以在x86上执行易失性读取操作而无需显式高速缓存失效,并且与正常变量读取一样快(忽略易失性的重新排序约束)?

我相信我没有正确理解这一点。 有人可以照顾开导我吗?

编辑:我不知道在多处理器环境中是否有差异。 正如John V.指出的那样,在单CPU系统上,CPU可能会看它自己的线程缓存,但在多CPU系统中,CPU必须有一些配置选项,这是不够的,主内存必须被击中,使得volatile变慢在多CP​​U系统上,对吗?

PS:在我学习更多关于这方面的方法中,我偶然发现了以下很棒的文章,因为这个问题可能对其他人很有趣,所以我会在这里分享我的链接:

  • Java理论与实践:修复Java存储模型,第1部分和
  • Java理论与实践:修复Java内存模型,第2部分

  • 在英特尔,一个非竞争性的易失性读取相当便宜。 如果我们考虑以下简单情况:

    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
    

    如果你看到2个引用getstatic,第一个涉及从内存加载,第二个跳过负载,因为值已从它已经加载到的寄存器中重用(长是64位,在我的32位笔记本电脑上它使用2个寄存器)。

    如果我们使得变量易变,那么生成的程序集是不同的。

    # {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引用都涉及来自内存的加载,即该值不能跨多个volatile读取保存在寄存器中。 为确保有原子读取,将数据从主存储器读取到MMX寄存器中movsd 0x6fb7b2f0(%ebp),%xmm0使读取操作成为单一指令(从前面的例子中我们看到64位值通常需要两个32位读取一个32位系统)。

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


    一般来说,在大多数现代处理器上,易失性负载与正常负载相当。 一个不稳定的商店大约是一个单一进入/监控退出时间的三分之一。 这是在缓存一致的系统上看到的。

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

    这是否意味着在x86上没有显式高速缓存失效可以完成易失性读操作,并且是一个快速的正常变量读取(忽略volatile的重新排序约束)?

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

    然而,我第二次尼尔的建议,如果你有一个字段访问多个线程你shold包装它作为一个AtomicReference。 作为一个AtomicReference,它执行的读/写吞吐量大致相同,但更明显的是该字段将被多个线程访问和修改。

    编辑回答OP的编辑:

    高速缓存一致性是一个复杂的协议,但总之:CPU将共享连接到主内存的公共高速缓存行。 如果一个CPU加载内存,并且没有其他CPU拥有该CPU,它将认为它是最新的值。 如果另一个CPU试图加载相同的内存位置,则已加载的CPU将会知道这一点,并将缓存的引用实际共享给请求的CPU - 现在请求CPU在其CPU缓存中拥有该内存的副本。 (它从来不需要在主内存中查找参考)

    涉及的协议有很多,但是这给出了正在发生的事情的概念。 同样为了回答您的其他问题,在没有多个处理器的情况下,使用多个处理器时,实际上易失性读取/写入可能会更快。 有一些应用程序实际上可以同时运行更快的单个CPU和多个应用程序。


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

    由于某些优化不可用,所得到的代码必然会比较慢,尽管可能不是很多。

    尽管如此,除非您知道它将从synchronized块之外的多个线程访问,否则不应该创建变量volatile 。 即使那样你也应该考虑volatile与synchronizedAtomicReference及其朋友,显式Lock类是否是最佳选择。

    链接地址: http://www.djcxy.com/p/96867.html

    上一篇: Is volatile expensive?

    下一篇: Difference between volatile and synchronized in Java