本文为学习笔记,原文地址为:time.geekbang.org/column/arti…
严守 MESI 协议的 CPU 会有啥问题?
严格遵守 MESI 协议的 CPU 设计,在它的某一个核在写一块缓存时,它需要通知所有的同伴:我要写这块缓存了,如果你们谁有这块缓存的副本,请把它置成 Invalid 状态。Invalid 状态意味着该缓存失效,如果其他 CPU 再访问这一缓存区时,就会从主存中加载正确的值。
这个过程相对于直接在核内缓存里修改缓存内容,非常漫长。这也就会导致,某个核请求独占时间比较长。那怎么来解决这个问题呢?
写缓冲与写屏障
CPU 的设计者为每个核都添加了一个名为 store buffer 的结构,store buffer 是硬件实现的缓冲区,它的读写速度比缓存的速度更快,所有面向缓存的写操作都会先经过 store buffer。
buffer 像是蓄水池,你可以理解成它是一个收作业的课代表,课代表会把所有同学的作业都收集齐以后再一次性地交到老师那里。buffer 中的数据没有副本,一旦丢失就彻底丢失了。store buffer 也是同样的道理,它会收集多次写操作,然后在合适的时机进行提交。
在这样的结构里,如果 CPU 的某个核再要对一个变量进行赋值,它就不必等到所有的同伴都确认完,而是直接把新的值放入 store buffer,然后再由 store buffer 慢慢地去做核间同步,并且将新的值刷入到 cache 中去就好了。而且,每个核的 store buffer 都是私有的,其他核不可见。
但用 store buffer 也会有一个问题,那就是它并不能保证变量写入缓存和主存的顺序
异常例子:
// CPU0
void foo() {
a = 1;
b = 1;
}
// CPU1
void bar() {
while (b == 0) continue;
assert(a == 1);
}
store buffer 在写入时,有可能 b 所对应的缓存行会先于 a 所对应的缓存行进入独占状态,也就是说 b 会先写入缓存
同时,CPU执行指令可能乱序,a和b的执行顺序不可意料
例子2:
// CPU0
void foo() {
a = 1;
b = a;
}
这个例子中,b 和 a 之间因为有数据依赖,是不可能乱序执行的,这就意味着上面我们分析的情况一是不会发生的。但由于 store buffer 的存在,情况二仍然可能发生,其结果就像我们上面分析的那样,CPU 执行第 10 行时会失败。这会让人感到更加匪夷所思。
为了解决这个问题,CPU 设计者就引入了内存屏障,屏障的作用是前边的读写操作未完成的情况下,后面的读写操作不能发生。
内存屏障保证了,其他 CPU 能观察到 CPU0 按照我们期望的顺序更新变量。
总的来说,store buffer 的存在是为提升写性能,放弃了缓存的顺序一致性,我们把这种现象称为弱缓存一致性。在正常的程序中,多个 CPU 一起操作同一个变量的情况是比较少的,所以 store buffer 可以大大提升程序的运行性能。但在需要核间同步的情况下,我们还是需要这种一致性的,这就需要软件工程师自己在合适的地方添加内存屏障了。
失效队列与读屏障
CPU 设计者引入了“invalid queue”,也就是失效队列这个结构。加入了这个结构后,收到 Invalid 消息的 CPU,比如我们称它为 CPU1,在收到 Invalid 消息时立即向 CPU0 发回确认消息,但这个时候 CPU1 并没有把自己的 cache 由 Share 置为 Invalid,而是把这个失效的消息放到一个队列中,等到空闲的时候再去处理失效消息,这个队列就是 invalid queue。
此时,缓存结构变成如下情况:
不过,放宽缓存一致性以后,缓存失效队列中的值如果还没有生效,可能就会出问题了,因此,这里也可以引入内存屏障,它会让 CPU 暂停执行,直到它处理完 invalid queue 中的失效消息之后,CPU 才会重新开始执行
综上,写屏障的作用是让屏障前后的写操作都不能翻过屏障。也就是说,写屏障之前的写操作一定会比之后的写操作先写到缓存中。读屏障的作用也是类似的,就是保证屏障前后的读操作都不能翻过屏障。假如屏障的前后都有缓存失效的信息,那屏障之前的失效信息一定会优先处理,也就意味着变量的新值一定会被优先更新。
总结
在 CPU 的具体实现中,通过放宽 MESI 协议的限制来获得性能提升。具体来说,我们引入了 store buffer 和 invalid queue,它们采用放宽 MESI 协议要求的办法,提升了写缓存核间同步的速度,从而提升了程序整体的运行速度。
但在这放宽的过程中,我们也看到会不断地出现新的问题,也就是说,一个 CPU 的读写操作在其他 CPU 看来出现了乱序。甚至,即使执行写操作的 CPU 并没有乱序执行,但是其他 CPU 观察到的变量更新顺序确实是乱序的。这个时候,我们就必须加入内存屏障来解决这个问题。
如何正确地使用内存屏障是一件很考验功底的事情,如果该加的地方没加,会带来非常严重的正确性问题。在操作系统,数据库,编译器等领域,会产生非常深远的影响,其代价甚至是完全无法接受的。而在不需要加的地方,如果你施加了比较重的屏障则可能带来性能下降,成为系统瓶颈。
具体内存屏障如何应用呢?且看下回JMM Java内存模型
引用
本文内容来自极客时间《编程高手必学的内存知识》第16讲,原文链接为:time.geekbang.org/column/arti…