挥发性贵吗?

在阅读了JSR-133编译器编写者关于易失性实现的食谱之后,特别是“与原子指令的交互”部分,我假设在不更新的情况下读取易失性变量需要LoadLoad或LoadStore障碍。在页面的下方,我看到LoadLoad和LoadStore在X86 CPU上实际上是无操作的。这是否意味着易失性读取操作可以在x86上没有显式缓存失效的情况下完成,并且与普通变量读取一样快(忽略易失性重新排序约束)?

我相信我没有正确地理解这一点。有人能关心我吗?

编辑:我想知道多处理器环境中是否存在差异。在单CPU系统上,CPU可能会查看它自己的线程缓存,正如John V.所说,但是在多CPU系统上,CPU必须有一些配置选项,这是不够的,并且必须击中主内存,这使得多CPU系统上的易失性变慢,对吧?

PS:在我了解更多有关此内容的途中,我偶然发现了以下精彩文章,并且由于其他人可能对这个问题感兴趣,因此我将在此处分享我的链接:


答案 1

在英特尔,无争议的易失性读取是相当便宜的。如果我们考虑以下简单情况:

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个引用,第一个涉及从内存加载,第二个引用跳过加载,因为该值已从已加载到的寄存器中重复使用(long是64位,在我的32位笔记本电脑上,它使用2个寄存器)。

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

在这种情况下,对变量 l 的两个 getstatic 引用都涉及来自内存的负载,即该值不能在多个易失性读取之间保存在寄存器中。为了确保存在原子读取,该值从主存储器读取到MMX寄存器中,使读取操作成为单个指令(从前面的示例中,我们看到64位值通常需要在32位系统上进行两次32位读取)。movsd 0x6fb7b2f0(%ebp),%xmm0

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


答案 2

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

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

这是否意味着易失性读取操作可以在x86上没有显式缓存失效的情况下完成,并且与普通变量读取一样快(忽略易失性重新排序的禁忌症)?

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

但是,我同意Neil的建议,即如果你有一个由多个线程访问的字段,你应该把它包装为一个AtomicReference。作为一个 AtomicReference,它执行大致相同的读取/写入吞吐量,但也更明显的是,该字段将由多个线程访问和修改。

编辑以回答OP的编辑:

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

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


推荐