内存屏障,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以执行此点之后的操作。内存屏障的产生是为了解决程序在运行过程中所产生的内存乱序访问问题。
若程序在执行时的实际内存访问顺序和程序代码编写的访问顺序不一致,这就是内存乱序访问。内存乱序访问的出现是为了提升程序执行时的效率,主要发生在两个阶段:
MESI协议解决了缓存一致性问题, 但是其自身也存在一个性能弱点——处理器执行写内存操作时,必须等待其他所有处理器将其高速缓存中的相应副本数据删除并接收到这些处理器所回复的 Invalidate Acknowledge/Read Response消息之后才能将数据写入高速缓存。
为了规避和减少这种等待造成的写操作的延迟 (Latency),硬件设计者引入了Store Buffer和Invalidate Queue,用于优化缓存一致性协议的性能,与此同时,也导致了一些内存乱序的问题。
Store Buffer是处理器内部的一个容量比Cache还小的私有高速存储部件,每个处理器都有其Store Buffer,并且一个处理器无法读取另外一个处理器上的Store Buffer中的内容。
引入Store Buffer后,处理在执行写操作时的行为如下:
缓存状态 | 操作 |
---|---|
Exclusive/Modified | 处理器可能会直接将数据写入相应的缓存行而无需发送任何消息(取决于具体处理器的实现) |
Shared | 处理器会先将写操作的相关数据存入Store Buffer中,并发送Read Invalidate消息 |
Invalid | 处理器的高速缓存不含有待操作的数据,遇到了写未命中(Write Miss),处理器会先将写操作的相关数据写入Store Buffer,并发送Read Invalidate消息,此后处理器继续运行 |
在写数据之前我们先要得到缓存行的独占权,如果当前CPU没有独占权,要先让系统中别的CPU上缓存的同一段数据都变成无效状态。为了提高性能,可以引入一个叫做Store Buffer的模块,将其放置在每个CPU和它的缓存之间。当前CPU发起写操作,如果发现没有独占权,可以先将要写入的数据放在存储缓冲中,并继续运行,仿佛独占权瞬间就得到了一样。当然,存储缓冲中的数据最后还是会被同步到缓存中的,但就相当于是异步执行了,不会让CPU等了。并且,当前CPU在读取数据的时候应该首先检查其是否存在于存储缓冲中。
允许处理器直接从Store Buffer中读取数据来实现内存读操作的技术被称为存储转发(Store Forwarding)。 存储转发使得写操作的执行处理器能够在不影响该处理器执行读操作的情况下将写操作的结果存入写缓冲器。
如果当前CPU上收到一条消息,要使某个缓存段失效,但是此时缓存正在处理其它事情,那这个消息可能无法在当前的指令周期中得到处理,而会将其放入所谓的Invalidation Queue中,同时立即发送使无效应答消息。那个待处理的使无效消息将保存在队列中,直到缓存有空为止。
内存屏障允许软件开发者使用硬件提供的特殊指令控制编译器和CPU的行为,在可能存在并发访问问题的点禁止编译器指令重排和CPU对指令乱序执行。为了解决上述的内存访问乱序问题,系统提供了两种类型的内存屏障:编译器屏障和CPU内存屏障。
编译器屏障用于阻止编译器进行指令重排,保证其在编译程序时,在优化屏障之前的指令不会在优化屏障之后执行。编译器提供了barrier() 宏实现编译器屏障:
#define barrier() __asm__ __volatile__("": : :"memory")
需要注意的一点是,barrier()只会约束编译时的指令重排行为,不约束CPU运行时的行为,CPU仍然可以乱序执行程序。
CPU内存屏障用于防止CPU运行时指令之间的重排序行为,并保证数据对其它CPU的可见性。CPU内存屏障通常可分为三类: