文章目录
cpu缓存一致性
缓存的出现:
- 提高内存的访问速度(时间、空间局部性),指令、数据的预取
多核之后带来的缓存一致性问题,如何解决
-
锁总线
-
MESI 协议(总线嗅探机制)+ 内存屏障
LOCK 指令(刚好可以实现上述的目标)
- 早期:锁总线(实现,性能差)
后期:锁缓存(实现,Ringbus + MESI协议,硬件支持,无需软件实现) - 内存屏障
LOCK 指令特性
- 硬件层面提供 lfence、sfence、mfence 三个内存屏障以及一个可以实现相同效果的 lock 指令前缀
- 一般lock都会加入读屏障,保证后续代码可以读到别的cpu核心上的未回写的缓存数据,而unlock都会加入写屏障,将所有的未回写的缓存进行回写。
内存屏障特性
- 所有的CPU内存屏障封装都隐式包含了编译器屏障。
- 只有内存屏障是无法保证缓存的同步的,还需要MESI一致性协议的支持
编译器屏障的作用
- 防止编译乱序
- 数据重新load缓存
MESI协议
- 保证了单条指令的在缓存上的读写的一致性
- MESI协议可以通过提供加入缓存带来的数据一致性问题,但是会带来一些性能的消耗,比如说缓存的伪共享
- MESI是强一致性,强一致性必定会带来性能的损害
为什么有了 MESI协议 还需要 内存屏障
- MESI是强一致性的,比如:需要等待写失效才能写入内存,所以硬件又引入了store buffer还有invalid queue,导致了有可能cpu 的乱序执行,为了禁止这种乱序执行需要加入内存屏障,但是这种乱序执行的前提是(数据之间没有依赖性)
问题:
-
那如果当前访问的数据在寄存器上面呢
- 需要 用到 volatile指令,强制从缓存中读取一次数据,间接通过MESI协议能够访问到内存中的数据
-
那如果是多条指令的顺序性呢,内存数据还在store buffer、invalid queue上面呢?
- 需要用到 内存屏障的指令,比如 x86 fence
总结:
- volatile指令的作用(cpp的volatile和java的特性不一致)
- c++ volatile 的特性
- 禁止编译器的优化
- 禁止编译器的代码的重排序
- 强制从缓存中读取,失效寄存器
- java volatile 的特性:是基于 LOCK 指令 实现的
- 在cpp的特性的基础之上
- 实现了一个全屏障(越过cpu的乱序执行、指令重排序等,保证了数据的顺序一致性)
- c++ volatile 的特性
- 缓存的一致性保证是通过 MESI 协议(总线嗅探机制)+ 内存屏障 实现的,因此为什么说 尽管有了 CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字(主要是增加屏障的目的,单靠MESI协议无法保证 整体顺序的一致性)
附带:
java
// (java 9) hotspot/src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp
// java实现的内存屏障
inline void OrderAccess::loadload() { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore() { compiler_barrier(); }
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::acquire() { compiler_barrier(); }
inline void OrderAccess::release() { compiler_barrier(); }
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl 0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl0,0(%%esp)" : : : "cc", "memory");
#endif
}
compiler_barrier();
}