C++中:
A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.
内存屏障(内存栅栏)
- 指令管道化
- CPU避免内存访问延迟
- 尽量重排这些管道的执行以最大化利用缓存
- 因缓存未命中引起的延迟降到最小
- 变量可能一直存在寄存器上,并没有被推到缓存或主存,这样这个变量对其他CPU来说一直都是不可见的
- CPU多级缓存
- 缓存和主存的读取会利用load, store和write-combining缓冲区来缓冲和重排
- Cache Conherence(缓存一致性)
- 一旦内存数据被推送到缓存,就会有消息协议来确保所有的缓存会对所有的共享数据同步并保持一致
- 使内存数据对CPU核可见的技术
- 两个功能
- 确保从另一个CPU来看屏障的两边的所有指令都是正确的程序顺序,而保持程序顺序的外部可见性
- 可以实现内存数据可见性,确保内存数据会同步到CPU缓存子系统
x86实现
- Store Barrier(Store屏障)
- x86的
sfence
指令- 强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行
- 把store缓冲区的数据都刷到CPU缓存
- 程序状态对其它CPU可见
- x86的
- Load Barrier(Load屏障)
- x86上的
ifence
指令- 强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行
- 一直等到load缓冲区被该CPU读完才能执行之后的load指令
- 从其它CPU暴露出来的程序状态对该CPU可见
- x86上的
- Full Barrier(Full屏障)
- x86上的
mfence
指令- 复合了load和save屏障的功能
- x86上的
Java中的volatile屏障
- 写操作之后会插入一个store屏障
- 在读操作之前会插入一个load屏障
Java中的final屏障
- 初始化后插入一个store屏障
- 来确保final字段在构造函数初始化完成并可被使用时可见
原子指令和Software Locks
- x86上的
lock ***
指令 - Full Barrier
- 执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU
- Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序。
性能影响
- 内存屏障阻碍了CPU采用优化技术来降低内存操作延迟,必须考虑因此带来的性能损失
- 注意边界
- 在边界时选择屏障
- 避免频繁触发屏障
- 可以在屏障前,尽量操作,已到达批量更新
对volatile字段(cursor)的写操作创建了一个内存屏障,这个屏障将刷新所有缓存里的值(或者至少相应地使得缓存失效)。
合并写(write combining)
- CPU为了减小读写时的延迟
- 即便有缓存,但是对于CPU还是太慢
- 合并写存储缓冲区(write combining store buffers)
- CPU缓存
- 高效的非链式结构的hash map
- 每个桶(bucket)通常是64个字节
- 缓存行(cache line)
- 缓存行是内存交换的实际单位
- 如果CPU需要访问的地址hash后的行尚不在缓存中,那么缓存中对应位置的缓存行会被清除,以便载入新的行
- 当CPU执行存储指令(store)时,它会尝试将数据写到离CPU最近的L1缓存
- 如果此时出现缓存未命中,CPU会访问下一级缓存
- 此时就会采用**合并写(write combining)**优化策略
- 如果此时出现缓存未命中,CPU会访问下一级缓存
- 存储缓冲区
- 64字节
- 维护了一个64位的字段
- 类似JVM中卡表(card table)
- 在读取缓存之前会先去读取缓冲区
- 如果我们能在缓冲区被传输到外部缓存之前将其填满,那么将大大提高各级传输总线的效率
- 这些缓冲区的数量是有限的,且随CPU模型而异
- 例如在Intel CPU中,同一时刻只能拿到4个。这意味着,在一个循环中,你不应该同时写超过4个不同的内存位置,否则你将不能享受到合并写(write combining)的好处。
- 超线程(hyper-threading),可能会有2个线程竞争同一个核的缓冲区
伪共享(False Sharing)
- 缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。
- 如果多个变量在同一个缓存行中,就会冲突,影响性能
- 伪共享
- 写竞争
- 运行在SMP系统中并行线程实现可伸缩性最重要的限制因素
- SMP: Symmetrical Multi-Processing
- 对称多处理机系统
- 多核CPU也类似
- 如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效
- 当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效
- Java内存布局(Java Memory Layout)
- 对于HotSpot JVM,所有对象都有两个字长的对象头
- 第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word
- 偏向锁
- 第二个字是对象所属类的引用
- 如果是数组对象还需要一个额外的字来存储数组的长度
- 每个对象的起始地址都对齐于8字节以提高性能
- 类似SSD中的4K对齐
- 对象字段的内存对齐(重排序)
- doubles (8) 和 longs (8)
- ints (4) 和 floats (4)
- shorts (2) 和 chars (2)
- booleans (1) 和 bytes (1)
- references (4/8)
- <子类字段重复上述顺序>
- 第一个字是由24位哈希码和8位标志位(如锁的状态或作为锁对象)组成的Mark Word
- 用7个long可以填充一个缓存行
- java 8 中的
@Contended
sun.misc.Contended
- Grizzly
LinkedTransferQueue
- java 7 中没有采用
PaddedAtomicReference
- java 7 中没有采用
- java 8 中的
- 对于HotSpot JVM,所有对象都有两个字长的对象头
// enough padding for 64bytes with 4byte refs Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
public final static class VolatileLong{ public volatile long value = 0L; public long p1, p2, p3, p4, p5, p6; // comment out}
参考资料