java并发编程的艺术 -(扩展:CPU缓存一致性)
cache的由来和带来的问题
cpu在摩尔定律的指导下,处理能力远远大于内存和硬盘的读写能力。就像用内存来解决硬盘的IO瓶颈一样,cache则是被用来处理cpu于内存之间的读写瓶颈。cache的工作原理基于局部性原理(解释如下):
- 时间局部性:如果某个数据被访问,那么不久将来它很可能再次被访问。
- 空间局部性:如果某个数据被访问,那么与它相邻的数据也可能被访问。
三级缓存
由于CPU的运算速度超越了1级缓存的数据I\O能力,CPU厂商又引入了多级的缓存结构。所以引入了多级缓存的概念,当下多核cpu的缓存一般都到了三级。
- L1 cache和Cpu及L2 cache交互
- L2 cache和L1 cache及L3 cache交互
- L3 cache和L2 cache和内存交互
缓存引发的问题
缓存一致性的问题。exp:core1、core2同时对内存变量i进行++操作,由于core直接操作的是L1 cache,因此两个core得到的结果都是2,两个core写回内存的结果都为2,而不是预期的3。
缓存一致性
为了解决缓存一致性的问题,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有MSI,MESI,MOSI等。这里简单介绍一下其中最经典的MESI协议。
在MESI协议中,每个cache line有4个状态,可用2个bit表示,如下表:
状态 | 描述 |
---|---|
M(Modified) | 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 |
E(Exclusive) | 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。 |
S(Shared) | 这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中。 |
I(Invalid) | 这行数据无效。 |
举个栗子:
一个3core的cpu,core0、core1、core2; 一个变量x。
- 只有core0访问了x,它的Cache line状态为E(Exclusive)。
- 3个core同时访问了x,它们对应的Cache line为S(Shared)状态。
- 在2的情况下,core0修改了x的值之后,这个Cache line变成了M(Modified)状态,其他Core对应的Cache line变成了I(Invalid)状态。
MESI引入的其他问题
缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中CPU都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。
CPU切换状态阻塞解决-存储缓存(Store Bufferes)
比如你需要修改本地缓存中的一条信息,那么你必须将I(无效)状态通知到其他拥有该缓存数据的CPU缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。
Store Bufferes(同步变异步)
为了避免这种CPU运算能力的浪费,Store Bufferes被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
Store Bufferes的风险
- 就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
- 保存什么时候会完成,这个并没有任何保证。
ps:引入了重排序(reordings)问题。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。它只是意味着其他的CPU会读到跟程序中写入的顺序不一样的结果。