CPU多核缓存架构
1、多线程环境下存在的问题
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(RAM)。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。
若有两个线程 T1 和 T2 都去计算 x + 1的值(x初始值为1),T1线程的由 CPU1去处理, T2 线程由CPU2去处理;CPU1和CPU2的高速缓存中的副本都是 x = 1,经过CPU加1操作后,再次放入的CPU1、CPU2的高速缓存中的数都为 x = 2 ,当数据放入到主内存中的时候,就引发了缓存不一致性的问题;
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议(缓存一致性协议),在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等。解决一致性的问题也可以使用总线加锁的方式,当CPU1读主内存同一段逻辑空间时,给总线加一把锁,CPU2读写的时候都没有办法进行,这样也可以解决该问题,但是这样的效率太低,一般不使用这种方式
2、MESI缓存一致性协议
CPU缓存的最小单位:缓存行(Cache line),有可能32字节、64字节、128字节,根据CPU来定;
(1)MESI的介绍
(2) MESI缓存一致性协议的工作原理
以上面多线程中计算 x 加1的操作为例;
(1)T1 从主存中读取 x = 1,放到 CPU1 的缓存,状态标记为独占(E),并且 CPU1 时刻监听总线当中其他CPU对这块内存的操作(总线嗅探机制);
(2)若 T1 计算之后还没有会写到主存中,此时 T2 由CPU2计算x加1的操作,从主存中读取x = 1,放入CPU2的缓存中;
(3)当 T2 通过总线从 主存中读取了 x = 1,T1的CPU1通过总线嗅探机制知道了T2从主存中读取了同块内存,则将 CPU1 中的状态修改为共享(S),
CPU2 中的状态标记为共享(S),同时 CPU2 时刻监听总线当中其他CPU对这块内存的操作;
(4)此时,T1通过 CPU1 计算完成了的结果:x = 2,需要将结果回写到主存中;首先锁住CPU1的缓存行,将CPU1 的缓存的状态标记为修改(M),接着发送消息给总线,其他的CPU一直在嗅探,嗅探到消息之后将其他的CPU的缓存状态标记为无效(I),此时CPU2的缓存状态被修改的无效;
(5)接着,CPU1 将计算结果 x = 2 回写到主存中(主存中的数据此时变为 x = 2),接着CPU1 将缓存的状态标记为独占(E), 丢弃掉其他CPU缓存状态为无效的数据;
(6)接着CPU2 重新从主存中读取数据(x = 2),通过总线嗅探机制CPU1又嗅探到CPU2读取了主存中的这块内存区域,则将 CPU1的缓存状态和CPU2的缓存状态都设置为共享(S)....
失效指的是缓存行的失效,
若两个CUP同一个时间都去修改主存中 x 的值,则在一个指令周期内会进行裁决,一个认为有效,另一个认为无效;有一个裁决为无效了,不一定再次去主存中读取了,取决于程序的写法;
比如在java中,变量前面增加 volatile 关键字,则在底层就去要遵守缓存一致性协议;
(3)什么情况下缓存一致性协议会失效?
情况一:
如果x 的存储长度大于缓存行的存储单元;数据存储横跨两个以上的缓存行,没有办法去给缓存行去加锁了,只能通过总线加锁的方式;
情况二:
CPU本身不支持缓存一致性协议,比如早期的奔腾系列CPU;
3、指令重排序问题
为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。