CPU 缓存一致性协议 MESI
================
CPU 高速缓存(Cache Memory)
CPU 在摩尔定律的指导下以每 18 个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及 CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而 CPU 的高度运算需要高速的数据。为了解决这个问题,CPU 厂商在 CPU 中内置了少量的高速缓存以解决 I\O 速度和 CPU 运算速度之间的不匹配问题。
在 CPU 访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。
时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
比如循环、递归、方法的反复调用等。
空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。
比如顺序执行的代码、连续创建的两个对象、数组等。
由于 CPU 的运算速度超越了 1 级缓存的数据 I\O 能力,CPU 厂商又引入了多级的缓存结构。
多级缓存结构
多核 CPU 多级缓存一致性协议 MESI
多核 CPU 的情况下有多个一级缓存,如何保证缓存内部数据的一致, 不让系统数据混乱。这里就引出了一个一致性的协议 MESI。
MESI 是指 4 中状态的首字母。每个 Cache line 有 4 个状态,可用 2 个 bit 表示,它们分别是:
缓存行(Cache line): 缓存存储数据的单元。
触发事件 | 描述 |
---|---|
本地读取(Local read) | 本地 cache 读取本地 cache 数据 |
本地写入(Local write) | 本地 cache 写入本地 cache 数据 |
远端读取(Remote read) | 其他 cache 读取本地 cache 数据 |
远端写入(Remote write) | 其他 cache 写入本地 cache 数据 |
2.cache 分类:
前提:所有的 cache 共同缓存了主内存中的某一条数据。
本地 cache: 指当前 cpu 的 cache。
触发 cache: 触发读写事件的 cache。
其他 cache: 指既除了以上两种之外的 cache。
注意:本地的事件触发 本地 cache 和触发 cache 为相同。
上图的切换解释:
M | E | S | I | |
---|---|---|---|---|
M | × | × | × | √ |
E | × | × | × | √ |
S | × | × | √ | √ |
I | √ | √ | √ | √ |
举个栗子来说:
假设 cache 1 中有一个变量 x = 0 的 cache line 处于 S 状态 (共享)。
那么其他拥有 x 变量的 cache 2、cache 3 等 x 的 cache line 调整为 S 状态(共享)或者调整为 I 状态(无效)。
假设有三个 CPU A、B、C,对应三个缓存分别是 cache a、b、 c。在主内存中定义了 x 的引用值为 0。
那么执行流程是:
CPU A 发出了一条指令,从主内存中读取 x。
从主内存通过 bus 读取到缓存中(远端读取 Remote read), 这是该 Cache line 修改为 E 状态(独享).
那么执行流程是:
CPU A 发出了一条指令,从主内存中读取 x。
CPU A 从主内存通过 bus 读取到 cache a 中并将该 cache line 设置为 E 状态。
CPU B 发出了一条指令,从主内存中读取 x。
CPU B 试图从主内存中读取 x 时,CPU A 检测到了地址冲突。这时 CPU A 对相关数据做出响应。此时 x 存储于 cache a 和 cache b 中,x 在 chche a 和 cache b 中都被设置为 S 状态 (共享)。
那么执行流程是:
CPU A 计算完成后发指令需要修改 x.
CPU A 将 x 设置为 M 状态(修改)并通知缓存了 x 的 CPU B, CPU B 将本地 cache b 中的 x 设置为 I 状态 (无效)
CPU A 对 x 进行赋值。
那么执行流程是:
CPU B 发出了要读取 x 的指令。
CPU B 通知 CPU A,CPU A 将修改后的数据同步到主内存时 cache a 修改为 E(独享)
CPU A 同步 CPU B 的 x, 将 cache a 和同步后 cache b 中的 x 设置为 S 状态(共享)。
MESI 优化和他们引入的问题
缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中 CPU 都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。
比如你需要修改本地缓存中的一条信息,那么你必须将 I(无效)状态通知到其他拥有该缓存数据的 CPU 缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。
为了避免这种 CPU 运算能力的浪费,Store Bufferes 被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
这么做有两个风险
第一、就是处理器会尝试从存储缓存(Store buffer)中读取值,但它还没有进行提交。这个的解决方案称为 Store Forwarding,它使得加载的时候,如果存储缓存中存在,则进行返回。
第二、保存什么时候会完成,这个并没有任何保证。
value = 3;
void exeToCPUA(){
value = 10;
isFinsh = true;
}
void exeToCPUB(){
if(isFinsh){
//value一定等于10?!
assert value == 10;
}
}
试想一下开始执行时,CPU A 保存着 finished 在 E(独享) 状态,而 value 并没有保存在它的缓存中。(例如,Invalid)。在这种情况下,value 会比 finished 更迟地抛弃存储缓存。完全有可能 CPU B 读取 finished 的值为 true,而 value 的值不等于 10。
即 isFinsh 的赋值在 value 赋值之前。
这种在可识别的行为中发生的变化称为重排序(reordings)。注意,这不意味着你的指令的位置被恶意(或者好意)地更改。
它只是意味着其他的 CPU 会读到跟程序中写入的顺序不一样的结果。
~顺便提一下 NIO 的设计和 Store Bufferes 的设计是非常相像的。~
执行失效也不是一个简单的操作,它需要处理器去处理。另外,存储缓存(Store Buffers)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列。它们的约定如下:
即便是这样处理器已然不知道什么时候优化是允许的,而什么时候并不允许。
干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(Memory Barriers)。
写屏障 Store Memory Barrier(a.k.a. ST, SMB, smp_wmb) 是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。
读屏障 Load Memory Barrier (a.k.a. LD, RMB, smp_rmb) 是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。
void executedOnCpu0() {
value = 10;
//在更新数据之前必须将所有存储缓存(store buffer)中的指令执行完毕。
storeMemoryBarrier();
finished = true;
}
void executedOnCpu1() {
while(!finished);
//在读取之前将所有失效队列中关于该数据的指令执行完毕。
loadMemoryBarrier();
assert value == 10;
}
www.importnew.com/10589.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。