【问题标题】:Where is the load barrier for the volatile statement?volatile 语句的负载障碍在哪里?
【发布时间】:2015-03-02 14:24:09
【问题描述】:

我编写了这个简单的 Java 程序:

package com.salil.threads;

public class IncrementClass {

    static volatile int j = 0;
    static int i = 0;

    public static void main(String args[]) {

        for(int a=0;a<1000000;a++);
        i++;
        j++;            
    }       
}

这将为 i++ 和 j++ 生成以下反汇编代码(删除剩余的反汇编代码):

  0x0000000002961a6c: 49ba98e8d0d507000000 mov       r10,7d5d0e898h
                                                ;   {oop(a 'java/lang/Class' = 'com/salil/threads/IncrementClass')}
  0x0000000002961a76: 41ff4274            inc       dword ptr [r10+74h]
                                                ;*if_icmpge
                                                ; - com.salil.threads.IncrementClass::main@5 (line 10)
  0x0000000002961a7a: 458b5a70            mov       r11d,dword ptr [r10+70h]
  0x0000000002961a7e: 41ffc3              inc       r11d
  0x0000000002961a81: 45895a70            mov       dword ptr [r10+70h],r11d
  0x0000000002961a85: f083042400          lock add  dword ptr [rsp],0h
                                                ;*putstatic j
                                                ; - com.salil.threads.IncrementClass::main@27 (line 14)

这是我对以下汇编代码的理解:

  • mov r10,7d5d0e898h : 将指向 IncrementClass.class 的指针移动到寄存器 r10
  • inc dword ptr [r10+74h] : 递增地址 [r10 + 74h],(i.e. i) 处的 4 字节值
  • mov r11d,dword ptr [r10+70h] :将地址[r10 + 70h]处的4个值的值移动到寄存器r11d(即把j的值移动到r11d)
  • inc r11d : 增加 r11d
  • mov dword ptr [r10+70h],r11d : 将 r11d 的值写入 [r10 + 70h] 使其对其他线程可见 -lock add dword ptr [rsp],0h : 锁定栈指针rsp表示的内存地址,并加0。

JMM 指出,在每次 volatile 读取之前必须有一个加载内存屏障,并且在每次 volatile 写入之后必须有一个存储屏障。我的问题是:

  1. 为什么在将 j 读入 r11d 之前没有负载屏障?
  2. 锁和添加到 rsp 如何确保将 r11d 中 j 的值传播回主内存。我从 intel 规范中读到的只是 lock 在操作期间为 cpu 提供了对指定内存地址的独占锁。

【问题讨论】:

  • 该代码非常糟糕。 lock inc dword [r10+70h] 会做 load/inc/store/full-barrier 所做的一切,甚至更多(即实际上是原子的)。它至少会一样快,而且代码字节数会少得多。 lock add [rsp], 0 是一个完整的障碍,因为每个 locked 指令都是。关于 MFENCE 或其他无操作锁定 insn 到堆栈内存(在 L1 中应该已经处于 E 状态)是否更好存在争议。 MFENCE 的吞吐量更差,但微指令更少,因此当 MFENCE 链不是您正在做的全部时,对周围指令的影响可能更小。
  • mov r10, imm64 也很可疑。这在循环里面???这是来自 JIT 的 优化 代码吗? inc r11d 是循环计数器,还是至少保存在寄存器中?
  • @SalilSurendran 我知道这是旧的,但语句不应该是在每次易失性读取之后必须有一个负载内存屏障吗?阅读之后,而不是之前

标签: java multithreading assembly intel


【解决方案1】:

Intel 处理器 x86 有一个strong memory model

因此,所有屏障 StoreStore 、 LoadLoad 、 LoadStore 在 x86 上都是无操作的。 StoreLoad 除外,可以通过 mfence 或 cpuid 或 locked insn 实现。 您已经可以使用您的汇编代码确认。其他障碍只是限制编译器优化和转换,以免破坏java memory model spec

当你在英特尔处理器上运行时,我假设它是 x86。

请阅读

  1. http://gee.cs.oswego.edu/dl/jmm/cookbook.html 供参考。

  2. http://psy-lob-saw.blogspot.com/2013/08/memory-barriers-are-not-free.html

  3. http://jsr166-concurrency.10961.n7.nabble.com/x86-NOOP-memory-barriers-td9991.html

Lock 不是指令,而是指令前缀(表现为 storeLoad 屏障)。

  1. What does the "lock" instruction mean in x86 assembly?
  2. Why we need lock prefix before CMPXCHG

【讨论】:

  • 我在发帖之前已经阅读了上面的大部分文章。为什么 i++ 不是原子或线程安全的。如果您查看递增 i "inc dword ptr [r10+74h]" 的指令,这应该直接写入内存,并且每个其他线程都应该能够看到这个值。据我了解,当 CPU 如上所述写入内存时,该值被缓存在缓存行中,并且不会一直进入内存,因此需要一条显式指令才能将其写入内存。我认为是 LOCK 语句,但是堆栈指针上的 LOCK 如何确保缓存中的值被写入内存。
  • 请看一下这个ans x86汇编中的“lock”指令是什么意思?。它非常清楚。 Inc 指令本身并不是读-修改-写操作,它只是增加了寄存器中的值,而如果以 lock 为前缀,它肯定会是。但是编译器编写者通过 inst lock 添加 0 值来实现 StoreLoad 屏障以产生这种效果。阅读答案很清楚
  • 好的。我相信以 LOCK 语句为前缀的任何指令都充当 StoreLoad 屏障,因为它可以防止在存储之前重新排序加载。 x86 的高速缓存一致性机制负责处理所有 CPU 看到这个已写入内存的值这一事实。我的理解正确吗?
  • 是的,请参阅 C++ 类似映射 cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html
  • @veritas 这是因为英特尔有这条规则:读取不会与任何读取一起重新排序?因此不会重新排序 volatile 读取(或任何其他读取)
【解决方案2】:

volatile Java 中的关键字只保证线程本地副本和缓存将被跳过,值将直接从主内存加载或写入主内存。但是它不包含锁定机制。因此,从volatile 读取或写入volatile 是原子的,但是一系列读写操作,就像你上面的那样

j++

不是原子的,因为其他一些线程可以在读取和写入主内存中的变量之间修改j 的值。要实现原子增量,您需要使用封装在 java 中的 Atomic 类中的 CAS 操作,例如 AtomicInteger 等。或者,如果您更喜欢低级编程,您可以在 Unsafe 类中使用原子方法,例如Unsafe.compareAndSwapInt

【讨论】:

  • 关于“线程本地副本和缓存”的论点由Wikipedia: volatile (computer programming) 支持。追本溯源C语言和中断处理程序也可以解释Java语言中借用和实现的语义
  • "Java 中的 volatile 关键字仅保证线程本地副本和缓存将被跳过,值将直接从主内存加载或写入主内存" - jmm 没有说类似这个并且没有生产级别的 JVM 像那样实现 volatile 。在 x86 上没有明确的障碍可见的原因是使用的指令已经提供了必要的可见性保证。
  • @Voo cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#volatile 也许我的术语和描述不准确,但这是相同的语义。
  • JMM 只讨论可见性保证和重新排序限制,但并不关心这些是如何实现的。在具有强大内存模型和缓存一致性的 x86 上,绝对没有必要写回内存,性能会很糟糕,通常不会这样做。除了名称之外,Java 的 volatile 和 C 的 volatile 也没有任何共同之处(C 的版本对于多线程编程完全没用,而 Java 的版本对 ISR 或硬件没有帮助)。请参阅JSR-113 cookbook 了解其一般实施方式。
  • @Salil 错误的假设,缓存没有被绕过,你可以很确定值没有写回内存(出于同样的原因,没有人使用直写缓存)。 LOCK 确实为您提供了必要障碍的原因是因为英特尔以这种方式指定了 ISA(有很多方法可以做到这一点,窥探其中一种)。
【解决方案3】:

障碍可能会被你的 JIT 编译器优化掉,因为你的程序是单线程的(只有一个线程——主线程),就像单线程环境下的锁可以优化掉一样。 这种优化与处理器架构无关。

【讨论】:

  • lock add [rsp], 0 是一个完整的屏障,与MFENCE 相同。不过,[r10+74] 的增量不是原子的,这很奇怪。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-26
  • 1970-01-01
  • 2015-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多