由于在现代计算的体系结构上,CPU的执行速度远远快于内存的访问速度,因此引入了诸如cache、分支预测、指令预取等技术以提高CPU的利用率。在多线程的编程环境下,这可能会带来一些问题,因此了解并掌握内存模型有助于我们在面对这些问题是有较为清晰的思路。
内存模型:描述多线程编程环境下,线程对内存的访问顺序
内存模型的定义应包含以下三个方面:
注意的是内存模型跟cpu的体系架构、编译器的实现以及编程语言规范都有关系。
内存操作顺序是指从某个角度观察到的对于内存的读和写所发生的顺序。着重强调的一点是内存操作顺序并不唯一。例如在一个有core0和core1的CPU中,core0/core1各有着自己的内存操作顺序,这两个内存操作顺序不一定相同。
memory order的问题就是因为指令重排引起的,指令重排导致原来的内存可见顺序发生了变化,在单线程执行起来的时候是没有问题的,但是放到多核/多线程执行的时候就出现问题了,为了效率引入的额外复杂逻辑的的弊端就出现了。
通过改变原有执行顺序而减少时间的执行过程称之为乱序执行,也称为重排,在单核情况下,乱序技术提高了运算速度。
(1)处理器乱序分类
现代处理器采用指令并行技术,在不存在数据依赖性的前提下,处理器可以改变语句对应的机器指令的执行顺序来提高处理器执行速度;
现代处理器采用内部缓存技术,导致数据的变化不能及时反映在主存所带来的乱序;
现代编译器为优化而重新安排语句的执行顺序;
(2)as-if-serial语义
无论是处理器还是编译器,不管怎么重排都要保证(单线程)程序的执行结果不能被改变,这就是as-if-serial语义.比如烧水煮茶的最终结果永远是煮茶,而不能变成烧水.为了遵循这种语义,处理器和编译器不能对存在数据依赖性的操作进行重排,因为这种重排会改变操作结果
CPU内存顺序可分为两种
Architecture | Memory Model |
---|---|
x86_64 | Total Store Order |
Sparc | Total Store Order |
ARMv8 | Weakly Ordered |
PowerPC | Weakly Ordered |
MIPS | Weakly Ordered |
x86_64和Sparc是强顺序模型(Total Store Order),这是一种接近程序顺序的顺序模型。所谓Total,就是说,内存(在写操作上)是有一个全局的顺序的(所有人看到的一样的顺序),就好像在内存上的每个Store动作必须有一个排队,一个弄完才轮到另一个,这个顺序和你的程序顺序直接相关。所有的行为组合只会是所有CPU内存程序顺序的交织,不会发生和程序顺序不一致的地方[4]。TSO模型有利于多线程程序的编写,对程序员更加友好,但对芯片实现者不友好。CPU为了TSO的承诺,会牺牲一些并发上的执行效率。
因为 store-load 可以被重排,所以x86不是顺序一致(sc)。但是因为其他三种读写顺序不能被重排,所以x86是 acquire/release 语义。
aquire语义:load 之后的读写操作无法被重排至 load 之前。即 load-load, load-store 不能被重排。
release语义:store 之前的读写操作无法被重排至 store 之后。即 load-store, store-store 不能被重排。
弱内存模型(简称WMO,Weak Memory Ordering),是把是否要求强制顺序这个要求直接交给程序员的方法。换句话说,CPU不去保证这个顺序模型(除非他们在一个CPU上就有依赖), 程序员要主动插入内存屏障指令来强化这个“可见性”[4]。ARMv8,PowerPC和MIPS等体系结构都是弱内存模型。每种弱内存模型的体系架构都有自己的内存屏障指令,语义也不完全相同。弱内存模型下,硬件实现起来相对简单,处理器执行的效率也高, 只要没有遇到显式的屏障指令,CPU可以对局部指令进行reorder以提高执行效率。
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
}