Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JMM和Linux内存屏障定义 #13

Open
seaswalker opened this issue Jul 5, 2020 · 0 comments
Open

JMM和Linux内存屏障定义 #13

seaswalker opened this issue Jul 5, 2020 · 0 comments

Comments

@seaswalker
Copy link
Owner

seaswalker commented Jul 5, 2020

JMM

根据: The JSR-133 Cookbook for Compiler Writers,Java内存模型定义了四种内存屏障:

  1. LoadLoad
  2. StoreStore
  3. LoadStore
  4. StoreLoad

其实就是load和store操作的全排列。

Linux

而Linux定义了三种内存屏障:

  1. 读屏障smp_rmb
  2. 写屏障smp_wmb
  3. 通用屏障smp_wb

关系

从字面上来看,smp_rmb对应LoadLoad,smp_wmb对应StoreStore,smp_mb对应LoadStore和StoreLoad,这样来说Java的定义更加细致一些,区分了Load和Store重排的两个不同方向。

读屏障

引用<Is Parallel Programming Hard, And, If So, What Can You Do About It?>中对它作用的说明:

The effect of this is that a read memory barrier orders only loads on the CPU that executes it, so that all loads preceding the read memory barrier will appear to have completed before any load following the read memory barrier.

重排

那么什么会导致相反地结果?我觉得可能有两种情况:

  1. 真正的乱序执行,即CPU重排两个load操作的顺序
  2. 隐式的重排:假设程序顺序上的第一条load操作cache miss,但第二条cache hit,那么可能第二条先于第一条完成

那么在这种情况下读屏障可以来保证顺序,怎么做到呢,猜测是禁止重排并且在第一条完成加载后再执行第二条。

Invalidate Queue

Is Parallel Programming Hard, And, If So, What Can You Do About It?书中给出了在存在Invalidate Queue的情况下读屏障的作用,代码如下:

// CPU 0执行,假设a处于share状态,b被CPU 0独占
void foo(void) {
    a = 1;
    smp_wmb();
    b = 1;
}

void bar(void) {
    while (b == 1) continue;
    assert (a == 1);
}

即使我们读到了b == 1,a还是有可能为0并导致断言失败。这里的原因并不是重排,而是执行bar函数的CPU没有将Invalidate Queue中的使无效消息应用到cache中,所以导致CPU读到了无效(过期)的a值。
所以需要一个读屏障来drain Invalidate Queue:

void bar(void) {
    while (b == 1) continue;
    smp_rmb();
    assert (a == 1);
}

所以我认为这里读屏障有两种实际的作用:禁止重排和drain Invalidate Queue

X86

  • X86平台没有Invalidate Queue,所有使无效消息都是实时处理的
  • X86保证了LoadLoad的先后顺序

这两点就保证了Java的LoadLoad和Linux的smp_rmb只是针对编译器的,而在硬件层面是一个nop

写屏障

通常CPU有store buffer这一结构(其实我也不知道是不是全都有,但X86和ARM是都存在)。假设CPU先后执行两个store操作:

a = 1;
b = 2;

和Load类似,我认为也有两种情形会导致b先于a被存储:

  1. 重排
  2. a没有被当前CPU在cache中独占,所以需要发送MESI消息来独占,在等待其它CPU响应期间将a扔到store buffer中。然后执行对b的存储,恰好b已经被当前CPU独占,那么就有可能实际上b先于a完成存储。

所以写屏障的作用就是:

  1. 禁止重排
  2. 在对b保存前如果发现存在写屏障,那么CPU要么将store buffer排干再处理b,要么把b也扔到store buffer,但是要保证store buffer FIFO

X86

x86平台会把所有写操作放到store buffer中,并保证写操作的先后顺序,所以在x86上StoreStore也是只针对编译器,对硬件是一个nop

LoadStore

x86保证了前面的load会先于store发生,至于原理不得而知,可能和store是异步的,而load是实时的有关。所以在x86上Java的LoadStore是一个只针对编译器的nop

StoreLoad

这是在x86上唯一没有被保证的,原因猜测是store是异步操作,load很容易跑到store前面。Java使用lock指令前缀来实现,而mfence也有同样的效果。
这一篇文章实锤了x86不保证StoreLoad: Memory Reordering Caught in the Act
mfence的作用是排干store buffer,在这之前封锁后续load操作。

但是是否x86保证了顺序,就意味着实际上完全没有重排发生了呢?可能不是这样的,重排可能还是有的,只不过是不可见的,比如对a和b的写入,两个变量恰好位于当前CPU的同一个缓存行,处于独占状态,其它CPU无法观察到两个变量写入的先后顺序。
参考: Does an x86 CPU reorder instructions?

总结

感觉内存屏障的细节真的很难理解,因为需要知道CPU内部的实现细节,而这一点则很难实现,比如对于ARM处理器就无法知道其store buffer是不是FIFO,有没有Invalidate Queue。

@seaswalker seaswalker changed the title 两种内存屏障定义 JMM和Linux内存屏障定义 Jul 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant