内存屏障与StoreBuffer #
CPU Store Buffer 的存在是为提升写性能,放弃了缓存的顺序一致性,我们把这种现象称为弱缓存一致性。在正常的程序中,多个 CPU 一起操作同一个变量的情况是比较少的,所以 store buffer 可以大大提升程序的运行性能。但在需要核间同步的情况下,我们还是需要这种一致性的,这就需要软件工程师自己在合适的地方添加内存屏障了。
store buffer并不能保证变量写入缓存和主存的顺序,你先来看看下面这个代码:
// CPU0
void foo() {
a = 1;
b = 1;
}
// CPU1
void bar() {
while (b == 0) continue;
assert(a == 1);
}
在这个代码块中,CPU0 执行 foo 函数,CPU1 执行 bar 函数。但在对变量 a 和 b 进行赋值的时候,有两种情况会导致它们的赋值顺序被打乱。
- CPU 的乱序执行。CPU 为了提升运行效率和提高缓存命中率,采用了乱序执行。
- store buffer 在写入时,有可能 b 所对应的缓存行会先于 a 所对应的缓存行进入独占状态,也就是说 b 会先写入缓存。
这种情况完全是有可能的。如果 a 是 Share 状态,b 是 Exclusive 状态,那么尽管 CPU0 在执行时没有乱序,这两个变量由 store buffer 写入缓存时也是不能保证顺序的。
那这个时候,我们假设 CPU1 开始执行时,a 和 b 所对应的缓存行都是 Invalid 状态。当 CPU1 开始执行第 9 行的时候,由于 b 所对应的缓存区域是 Invalid 状态,它就会向总线发出 BusRd 请求,那么 CPU1 就会先把 b 的最新值读到本地,完成变量 b 的值的更新,从而跳出while的循环,继续执行assert 语句。
这时,CPU1 的 a 缓存区域也处于 Invalid 状态,它也会产生 BusRd 请求,但我们前面分析过,CPU0 中对 a 的赋值可能会晚于 b,所以此时 CPU1 在读取变量 a 的值时,加载的就可能是老的值,也就是 0,那这个时候第 10 行的 assert 就会执行失败。
我们再举一个更极端的例子分析一下:
// CPU0
void foo() {
a = 1;
b = a;
}
这个例子中,b 和 a 之间因为有数据依赖,是不可能乱序执行的,这就意味着上面我们分析的情况一是不会发生的。但由于 store buffer 的存在,情况二仍然可能发生,其结果就像我们上面分析的那样,CPU 执行第 10 行时会失败。这会让人感到更加匪夷所思。
为了解决这个问题,CPU 设计者就引入了内存屏障,屏障的作用是前边的读写操作未完成的情况下,后面的读写操作不能发生。这就是 Arm 上 dmb 指令的由来,它是数据内存屏障(Data Memory Barrier)的缩写。
我们还是继续沿用前面 CPU0 和 CPU1 的例子,不过这一次我加入了内存屏障:
// CPU0
void foo() {
a = 1;
smp_mb();
b = 1;
}
// CPU1
void bar() {
while (b == 0) continue;
assert(a == 1);
}
在这里,smp_mb 就代表了多核体系结构上的内存屏障。由于在不同的体系结构上,指令各不相同,我们使用一个函数对它进行封装。加上这一道屏障以后, CPU 会保证 a 和 b 的赋值指令不会乱序执行,同时写入 cache 的顺序也与程序代码保持一致。
所以说,内存屏障保证了,其他 CPU 能观察到 CPU0 按照我们期望的顺序更新变量。