内存一致性模型
Cache coherence
本文主要讨论的是内存一致性问题(memory consistency),和缓存一致性(cache coherence)是不同的。在《计算机体系结构:量化方法研究》第五章中,memory consistency是由cache coherence引出的,所以我们就先来说说cache coherence吧。考虑下图:
如图,A和B读取X到缓存后,A直写(write through)X的值为0,但此时B缓存中存储的X的值仍然是1。即如果此时B读取X,将会收到数值1!
通俗地说,如果在每次读取某一数据项时都会返回该数据项的最新写入值,则称这个存储系统是一致的。这一定义有些含糊和简单,却包含了两个关键的方面:
1.coherence确定了读取操作可能返回什么值;
2.consistency确定了写入值什么时候读取操作返回。
简单来说,coherence其实保证的就是对某一个地址的读操作返回的值一定是那个地址的最新值(注意coherence和consistency最大的区别就是是对某一个地址还是全局),而这个最新值可能是该线程所处的CPU核心刚刚写进去的那个最新值,也可能是另一个CPU核心上的线程刚刚写进去的最新值。具体严谨的定义我直接从书上截图好了:
这三个条件只是保证了coherence,还未考虑consistency的问题。比如,如果一个处理器对X的写入操作仅比另一个处理器对X的读取操作提前很短的一点时间,那就不可能确保该读取操作会返回这个写入值。这个写入值多久后能确保被读取操作读取到,这正是memory consistency讨论的问题。
Memory consistency model
这部分我主要参考的是《Shared Memory Consistency Models:A Tutorial》这篇文章。先说一下最简单的顺序一致性(Sequential consistency)。定义如下:
Definition: [A multiprocessor system is sequentially consistent if] the result of any execution is
the same as if the operations of all the processors were executed in some sequential order, and the
operations of each individual processor appear in this sequence in the order specified by its program.
这个定义包含两个方面:1.在每个处理器内,维护程序次序;
2.在所有处理器间维护一个单一的操作次序,即所有处理器看到的操作次序要一样。这就使内存操作需要有原子性(或瞬发性)。
图 a 阐述了 SC 对程序次序的要求(要求一)。当处理器 P1 想要进入临界区时,
它先将 Flag1 更新为 1, 再去判断 P2 是否尝试进入临界区(Flag2). 若 Flag2 为 0,
表示 P2 未尝试进入临界区,此时 P1 可以安全进入临界区。这个算法假设如果 P1 中读到 Flag2 等于0, 那么P1的写操作(Flag1=1)会在P2
的写操作和读操作之前发生,这可以避免 P2 也进入临界区。SC 通过维护程序次序来保证这一点。
图 b 阐述了原子性要求。原子性允许我们假设 P1 对 A 的写操作对整个系统(P2, P3) 是同时可见的:P1 将 A 写成1; P2 先看到 P1 写 A 后才写 B;P3 看到 P2 写 B 后才去读 A, P3 此时读出的 A 必须要是 1 (而不会是0)因为从 P2 看来操作执行次序为 (A=1)->(B=1), 不允许P3在看到操作 B=1 时,没有看到 A=1.
实现SC需要付出代价,使性能降低,同时也会限制编译器的优化。为了获得更好的性能,引入宽松内存一致性模型(relaxed memory consistency models).
根据《量化》一书,relaxed memory consistency models有多种模型,比如放松W->R;放松W->W;放松 R->W 和 R->R,这包括多种模型,其中Release consistency我们单独拿出来说。
Release consistency
Release consistency包含两个同步操作,acquire和release。
1) ACQUIRE: 对于所有其它参与者来说, 在此操作后的所有读写操作必然发生在ACQUIRE这个动作之后。
2) RELEASE: 对于所有其它参与者来说, 在此操作前的所有读写操作必然发生在RELEASE这个动作之前。
注意, 这其中任意一个操作, 都只保证了一半的顺序:
对于ACQUIRE来说, 并没保证ACQUIRE前的读写操作不会发生在ACQUIRE动作之后.
对于RELEASE来说, 并没保证RELEASE后的读写操作不会发生在RELEASE动作之前.
因此release和acquire的组合形成了内存屏障。举个例子
处理器A执行以下代码:
a.store(3); b.store(4); m.store(5, release);
处理器B执行以下代码:
m.load(acquire);
g.load();
h.load();
release和acquire组成的内存屏障保证了当线程 B 执行完m.load(acquire);之后, 线程 B 必须已经能看到a==3,b==4,且g和h的load操作不会发生在处理器A的release操作之前。