内存模型(一)
并行系统给程序员带来了性能的提升,不过也给编程带来了挑战。load/store是程序员再熟悉不过的内存操作了,在单处理器的情况下,程序的执行是串行化的,内存呈现给程序员的表现就是program order。在多处理器的情况下,多个处理器共享内存,并行的执行程序,那么需要一种模型来决定内存对程序员的呈现形式,这也就是所谓的内存一致性模型。
内存的一致性模型有很多种,顺序一致性(Sequential consistency)是一种最严格、最容易想到的模型。内存是串行化的部件,同一时刻只有一个操作执行,这个顺序是全局的,对所有处理器都是相同的,自然可以根据顺序来规定内存一致性模型。顺序一致性有二个特征,一是单个处理器内部保持program order;二是所有的处理器看到的内存操作顺序都是一致的。
顺序一致性是一种理想化的模型,他使得程序员可以安全的把在单处理器正常运行的程序拿到多处理器上,而不需要加入额外的同步或内存屏障(memory barrier)。顺序一致性对内存操作的顺序要求过于严格,禁止了许多硬件和处理器的优化。
多发射,乱序执行等是现代CPU常见的流水线优化技术,这些reorder仅仅是保证了当前的CPU的程序的正确性,并不会考虑其他CPU上执行的程序。例如下面的例子:
flag1=flag2=0
P1 P2
flag1=1 flag2=1
if(flag2 == 0) if(flag1 == 0)
critical section critical section
P1可能会先load flag2,P2先load flag1,之后P1 store flag1,P2 store flag2,这样就导致了P1和P2同时进入临界区,违反了顺序一致性。这种情况有可能是编译器的reorder,也有可能是store的结果在load flag之前还没有写回内存,而是在write buffer内,这是一种W-R Order问题。
在NUMA体系结构上,CPU对不同的模块的访问时间是不一样的,两个store即时是按照program order发射,但他们的完成时间的顺序也不一样。考虑下面这个例子:
Head = Data = 0;
P1 P2
Data = 2000; while(Head == 0);
Head = 1; NewData = Data;
如果告诉你最终的结果有可能是 Head:1;Data:2000;NewData:0,你肯定会很惊讶,事实上,假设访存Data所在内存的时间可能会比Head多,那么这段代码的完成顺序就有可能是Head = 1,此时P2的while循环结束,读到老的Data值给NewData,最后才完成Data = 2000。这是W-W Order被违反的例子。
要解决之前的这些reorder,首先要提高一个机制把最新的写值广播到所有的cache拷贝,就是cache coherence protocol,其次需要能够检测到该内存写操作何时完成(比如写应答),从而不出现上面的W-W order问题。另外就是要能够保证写原子性,这就能解决之前W-R order问题。
顺序一致性的严格性使得它的性能不行,现代CPU体系结构基本上都对顺序一致性做了些松弛,放宽了一个或几个条件,出现了松弛的内存模型。见下表:
Relaxation | W-R Order | W-W Order | R-RW Order | Read Others' Write Early | Read Own Write Early |
IBM 370 | * | ||||
TSO | * | * | |||
PC | * | * | * | ||
PSO | * | * | * | ||
Alpha | * | * | * | * | |
PowerPC | * | * | * | * | * |
放宽Write to Read的Program Order
这就意味着之前的W-R例子虽然违反了顺序一致性模型,但是在这种模型中是允许发生的。
IBM 370,TSO,PC这三种体系结构都属于这种松弛模型,但也有不同,就是后面两项,这关系到写操作的原子性,IBM 370比其他两种更严格,它不能读之前的write,直到这个write被所有的处理器可见。因为IBM 370自然的满足了写原子性。
我们来看一个例子来理解这三种内存模型的差异:
A=flag1=flag2=0;
P1 P2
flag1 = 1; flag2 = 1;
A = 1; A = 2;
r1 = A; r3 = A;
r2 = flag2; r4 = flag1;
result: r1=1; r2=0; r3=2; r4=0
这个结果在TSO和PC结构下是合法的。因为在P1的flag2的load可以在flag1的store之前发生,同理P2的flag1的load可以在flag2的store之前。但是IBM 730的结构下这个结构是非法的,因为r1 = A不会发射,直到A = 1被执行完,A = 1执行完就意味着flag1 = 1也执行结束。
A = B = 0;
P1 P2 P3
A = 1; if(A == 1) if(B == 1)
B = 1; r1 = A;
result: B = 1; r1 = 0;
这个结果在PC下是合法的,r1可以在A = 1对P2可见,而对P3还没可见的时候就load A的值。而对IBM 370和TSO则只能在A = 1对所有处理器可见的时候才会发射r1 = A。
除了W-R Order,我们还可以继续放宽顺序一致性的条件,比如PowerPC就是把所有的Program Order都放宽了,这样可以获得最大的硬件和编译器的优化选项。但这样也同样给了程序员带来了烦恼,虽然这样内存模型对上面那些case有了最大的容忍性,但是显然那些令人惊讶的结果并不是程序员想要的,这些松弛的内存模型必须能够提供一些同步机制,来让程序员显示的来保证顺序一致性语义。所以松弛内存模型获得的比顺序一致性更好的性能,也是以编程的复杂性为代价换来的。下面这表就是各种松弛的内存模型提供的保证顺序一致性的safety net。
Relaxation | safety net |
IBM 370 | serialization instructions |
TSO | Read-Modify-Write(RMW) |
PC | RMW |
PSO | RMW, STBAR |
Alpha | Memory Barrer(MB), Write Memory Barrer(WMB) |
PowerPC | SYNC |
程序员可以利用这些safety net,加入代码中来保证程序语义上的顺序一致性。