上篇
CPU缓存的影响
缓存的内存操作被系统交叉感知的方式,在一定程度上,受到CPU和内存之间的缓存、以及保持系统一致状态的内存一致性系统的影响。
若CPU和系统其它部分的交互通过cache进行,内存系统就必须包括CPU缓存,以及CPU及其缓存之间的内存屏障(内存屏障逻辑上如下图中的虚线):
<--- CPU ---> : <----------- Memory ----------->
:
+--------+ +--------+ : +--------+ +-----------+
| | | | : | | | | +--------+
| CPU | | Memory | : | CPU | | | | |
| Core |--->| Access |----->| Cache |<-->| | | |
| | | Queue | : | | | |--->| Memory |
| | | | : | | | | | |
+--------+ +--------+ : +--------+ | | | |
: | Cache | +--------+
: | Coherency |
: | Mechanism | +--------+
+--------+ +--------+ : +--------+ | | | |
| | | | : | | | | | |
| CPU | | Memory | : | CPU | | |--->| Device |
| Core |--->| Access |----->| Cache |<-->| | | |
| | | Queue | : | | | | | |
| | | | : | | | | +--------+
+--------+ +--------+ : +--------+ +-----------+
:
:
虽然一些特定的load或store实际上可能不出现在发出这些指令的CPU之外,因为在该CPU自己的缓存内已经满足,但是,如果其它CPU关心这些数据,那么还是会产生完整的内存访问,因为高速缓存一致性机制将迁移缓存行到需要访问的CPU,并传播冲突。
只要能维持程序的因果关系,CPU核心可以以任何顺序执行指令。有些指令生成load和store操作,并将他们放入内存请求队列等待执行。CPU内核可以以任意顺序放入到队列中,并继续执行,直到它被强制等待某一个指令完成。
内存屏障关心的是控制访问穿越CPU到内存一边的顺序,以及系统其他组建感知到的顺序。
[!]对于一个给定的CPU,并不需要内存屏障,因为CPU总是可以看到自己的load和store指令,好像发生的顺序就是程序顺序一样。
[!]MMIO或其它设备访问可能绕过缓存系统。这取决于访问设备时内存窗口属性,或者某些CPU支持的特殊指令。
缓存一致性
但是事情并不像上面说的那么简单,虽然缓存被期望是一致的,但是没有保证这种一致性的顺序。这意味着在一个CPU上所做的更改最终可以被所有CPU可见,但是并不保证其它的CPU能以相同的顺序感知变化。
考虑一个系统,有一对CPU(1&2),每一个CPU有一组并行的数据缓存(CPU 1有A / B,CPU 2有C / D):
:
: +--------+
: +---------+ | |
+--------+ : +--->| Cache A |<------->| |
| | : | +---------+ | |
| CPU 1 |<---+ | |
| | : | +---------+ | |
+--------+ : +--->| Cache B |<------->| |
: +---------+ | |
: | Memory |
: +---------+ | System |
+--------+ : +--->| Cache C |<------->| |
| | : | +---------+ | |
| CPU 2 |<---+ | |
| | : | +---------+ | |
+--------+ : +--->| Cache D |<------->| |
: +---------+ | |
: +--------+
:
假设该系统具有以下属性:
- 奇数编号的缓存行在缓存A或者C中,或它可能仍然驻留在内存中;
- 偶数编号的缓存行在缓存B或者D中,或它可能仍然驻留在内存中;
- 当CPU核心正在访问一个cache,其它的cache可能利用总线来访问该系统的其余组件
—— 可能是取代脏缓存行或预加载; - 每个cache有一个操作队列,用来维持cache与系统其余部分的一致性;
- 正常load已经存在于缓存行中的数据时,一致性队列不会刷新,即使队列中的内容可能会影响这些load。
接下来,试想一下,第一个CPU上有两个写操作,并且它们之间有一个write屏障,来保证它们到达该CPU缓存的顺序:
CPU 1 CPU 2 COMMENT
=============== =============== =======================================
u == 0, v == 1 and p == &u, q == &u
v = 2;
smp_wmb(); Make sure change to v is visible before
change to p
v is now in cache A exclusively
p = &v;
p is now in cache B exclusively
write内存屏障强制系统中其它CPU能以正确的顺序感知本地CPU缓存的更改。现在假设第二个CPU要读取这些值:
CPU 1 CPU 2 COMMENT
=============== =============== =======================================
...
q = p;
x = *q;
上述一对读操作可能不会按预期的顺序执行,持有P的缓存行可能被第二个CPU的某一个缓存更新,而持有V的缓存行在第一个CPU的另外一个缓存中因为其它事情被延迟更新了;
CPU 1 CPU 2 COMMENT
=============== =============== =======================================
u == 0, v == 1 and p == &u, q == &u
v = 2;
smp_wmb();
p = &v; q = p;
x = *q;
Reads from v before v updated in cache
基本上,虽然两个缓存行CPU 2在最终都会得到更新,但是在不进行干预的情况下不能保证更新的顺序与在CPU 1在提交的顺序一致。
所以我们需要在load之间插入一个数据依赖屏障或read屏障。这将迫使缓存在处理其他任务之前强制提交一致性队列;
CPU 1 CPU 2 COMMENT
=============== =============== =======================================
u == 0, v == 1 and p == &u, q == &u
v = 2;
smp_wmb();
p = &v; q = p;
smp_read_barrier_depends()
x = *q;
Reads from v after v updated in cache
DEC Alpha处理器上可能会遇到这类问题,因为他们有一个分列缓存,通过更好地利用数据总线以提高性能。虽然大部分的CPU在读操作需要读取内存的时候会使用数据依赖屏障,但并不都这样,所以不能依赖这些。
其它CPU也可能页有分列缓存,但是对于正常的内存访问,它们会协调各个缓存列。在缺乏内存屏障的时候,Alpha 的语义会移除这种协作。
缓存一致性与DMA
对于DMA的设备,并不是所有的系统都维护缓存一致性。这时访问DMA的设备可能从RAM中得到脏数据,因为脏的缓存行可能驻留在各个CPU的缓存中,并且可能还没有被写入到RAM。为了处理这种情况,内核必须刷新每个CPU缓存上的重叠位(或是也可以直接废弃它们)。
此外,当设备以及加载自己的数据之后,可能被来自CPU缓存的脏缓存行写回RAM所覆盖,或者当前CPU缓存的缓存行可能直接忽略RAM已被更新,直到缓存行从CPU的缓存被丢弃和重载。为了处理这个问题,内核必须废弃每个CPU缓存的重叠位。
。### CPU能做的事情
程序员可能想当然的认为CPU会完全按照指定的顺序执行内存操作,如果确实如此的话,假设CPU执行下面这段代码:
a = *A;
*B = b;
c = *C;
d = *D;
*E = e;
他们会期望CPU执行下一个指令之前上一个一定执行完成,然后在系统中可以观察到一个明确的执行顺序;
LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E.
当然,现实中是非常混乱的。对许多CPU和编译器来说,上述假设都不成立,因为:
- load操作可能更需要立即完成的,以保持执行进度,而推迟store往往是没有问题的;
- load操作可能预取,当结果证明是不需要的,可以丢弃;
- load操作可能预取,导致取数的时间和预期的事件序列不符合;
- 内存访问的顺序可能被重排,以更好地利用CPU总线和缓存;
- 与内存和IO设备交互时,如果能批访问相邻的位置,load和store可能会合并,以提高性能,从而减少了事务设置的开销(内存和PCI设备都能够做到这一点);
- CPU的数据缓存也可能会影响顺序,虽然缓存一致性机制可以缓解 —— 一旦store操作命中缓存 —— 并不能保证一致性能正确的传播到其它CPU。
所以对另一个CPU,上面的代码实际观测的结果可能是:
LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B
(Where "LOAD {*C,*D}" is a combined load)
但是,CPU保证自身的一致性:不需要内存屏障,也可以保证自己以正确的顺序访问内存,如下面的代码:
U = *A;
*A = V;
*A = W;
X = *A;
*A = Y;
Z = *A;
假设不受到外部的影响,最终的结果可能为:
U == the original value of *A
X == W
Z == Y
*A == Y
上面的代码CPU可能产生的全部的内存访问顺序如下:
U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A
对于这个顺序,如果没有干预,在保持一致的前提下,一些操作也可能被合并,丢弃。
在CPU感知这些操作之前,编译器也可能合并、丢弃、延迟加载这些元素。
例如:
*A = V;
*A = W;
可减少到:
*A = W;
因为在没有write屏障的情况下,可以假定将V写入到*A的操作被丢弃了,同样:
*A = Y;
Z = *A;
若没有内存屏障,可被简化为:
*A = Y;
Z = Y;
在CPU之外根本看不到load操作。
ALPHA处理器
DEC Alpha CPU是最松散的CPU之一。不仅如此,一些版本的Alpha CPU有一个分列的数据缓存,允许它们在不同的时间更新语义相关的缓存。在同步多个缓存,保证一致性的时候,数据依赖屏障是必须的,使CPU可以正确的顺序处理指针的变化和数据的获得。
Alpha定义了Linux内核的内存屏障模型。
使用示例
循环缓冲区
内存屏障可以用来实现循环缓冲,不需要用锁来使得生产者与消费者串行。
参考
- Alpha AXP Architecture Reference Manual, Second Edition (Sites &
Witek, Digital Press) - AMD64 Architecture Programmer’s Manual Volume 2: System Programming
- IA-32 Intel Architecture Software Developer’s Manual, Volume 3: System Programming Guide
- The SPARC Architecture Manual, Version 9
- UltraSPARC Programmer Reference Manual
- UltraSPARC III Cu User’s Manual
- UltraSPARC IIIi Processor User’s Manual
- UltraSPARC Architecture 2005
- UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005
- Solaris Internals, Core Kernel Architecture, p63-68:
- Unix Systems for Modern Architectures, Symmetric Multiprocessing and
Caching for Kernel Programmers: - Intel Itanium Architecture Software Developer’s Manual: Volume 1:
原文 Memory barriers
作者 David Howells、Paul E. McKenney
译者 曹姚君
校对 丁一
via ifeve.com