内存屏障
1.Cpu Core Design
1.1Store Buffer
假设运行在 core0 上的 set_a() 对整型变量 a 赋值 1,计算机通常不会直接写穿通到内存,而是会在 Cache 中修改对应 Cache Line,每个 Core 都有独立的 Store Buffer,每个 Core 都访问私有的 Store Buffer,Store Buffer 帮助 CPU 遮掩了 Store 操作带来的延迟。
- 如果 Core0 的 Cache 里没有 a,赋值操作(store)会造成 Cache Miss
- Core0 会 stall 在 等 待 Cache 就 绪(从 内 存 加 载 变 量 a 到 对 应 的 Cache Line),但 Stall 会损害 CPU 性能,相当于 CPU 在这里停顿,白白浪费着宝贵的 CPU 时间
- 有了 Store Buffer,当变量在 Cache 中没有就位的时候,就先 Buffer 住这个Store 操作,而 Store 操作一旦进入 Store Buffer,core 便认为自己 Store完成,当随后 Cache 就位,store 会自动写入对应 Cache。
CPU Core在 Core Load 数据的时候,先检查 Store Buffer 中是否有悬而未决的 a 的新值,如果有,则取新值;否则从 cache 取 a 的副本。这种技术在多级流水线 CPU设计的时候就经常使用,叫 Store Forwarding。但是多核执行时还是可能会有问题,比如下面的代码会因为断言失败抛出异常:
int a = 0; // 被 CPU1 Cache
int b = 0; // 被 CPU0 Cache
// CPU0执行
void x() {
a = 1;
b = 2;
}
// CPU1执行
void y() {
while (b == 0);
assert(a == 1);
}1.2.Invalidate Queue
当一个变量加载到多个 core 的 Cache,则这个 Cache Line 处于 Shared 状态,如果 Core1 要修改这个变量,则需要通过发送核间消息 Invalidate 来通知其他 Core把对应的 Cache Line 置为 Invalid,当其他 Core 都 Invalid 这个 CacheLine 后,则本 Core 获得该变量的独占权,这个时候就可以修改它了。收到 Invalidate 消息的 core 需要回 Invalidate ACK,一个个 core 都这样 ACK,等所有 core 都回复完,Core1 才能修改它,这样 CPU 就白白浪费。
事实上,其他核在收到 Invalidate 消息后,会把 Invalidate 消息缓存到 Invalidate Queue,并立即回复 ACK,真正 Invalidate 动作可以延后再做,这样一方面因为Core 可以快速返回别的 Core 发出的 Invalidate 请求,不会导致发生 Invalidate请求的 Core 不必要的 Stall,另一方面也提供了进一步优化可能,比如在一个
CacheLine 里的多个变量的 Invalidate 可以攒一次做了。
2.内存屏障
内存屏障(Memory Barrier),也称内存栅栏、屏障指令等,是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,同步点之前的所有读写操作都执行后,才可以开始执行此点之后的操作。语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。内存屏障,其实就是提供一种机制,确保代码里顺序写下的多行,会按照书写的顺序,被存入内存,主要是解决 Store Buffer 引入导致的写入内存间隙的问题。
- 对于Store Buffer 里的所有数据项
我们可以插入一个写屏障,它会给 Store Buffer 里的所有数据项做一个标记(marked),然后继续执行下面的逻辑。简而言之,Core 执行到 write memory barrier(wmb)的时候,如果 Store Buffer还有悬而未决的 store 操作,则都会被 mark 上,直到被标注的 Store 操作进入内存后,后续的 Store 操作才能被执行,因此 wmb 保障了 barrier 前后操作的顺序,它不关心 barrier 前的多个操作的内存序,以及 barrier 后的多个操作的内存序,是否
与 Global Memory Order 一致。 - 对于Invalidate Queue 里的所有数据项
Invalidate Queue 的引入会影响 Load 的内存一致性,因为 Invalidate queue 会缓存其他核发过来的消息,比如Invalidate 某个数据的消息被 delay 处置,导致 core 在 Cache Line 中命中这个数据,而这个 Cache Line 本应该被 Invalidate 消息标记无效。
软件的思路,就是仿照 wmb() 的定义,加入 rmb() 约束。rmb() 给我们的 invalidate queue 加 上 标 记。 当 一 个 load 操 作 发 生 的 时 候, 之 前 的 rmb() 所 有 标 记 的invalidate 命令必须全部执行完成,然后才可以让随后的 load 发生。这样,我们就在 rmb() 前后保证了 load 观察到的顺序等同于 global memory order。