CPU 缓存一致性协议 MESI

CPU 高速缓存(Cache Memory)

CPU 为什么要有高速缓存

CPU 在摩尔定律的指导下以每 18 个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及 CPU。这就形成了高性能能的内存和硬盘价格及其昂贵。然而 CPU 的高度运算须要高速的数据。为了解决这个问题,CPU 厂商在 CPU 中内置了少许的高速缓存以解决 I\O 速度和 CPU 运算速度之间的不匹配问题。html

在 CPU 访问存储设备时,不管是存取数据抑或存取指令,都趋于汇集在一片连续的区域中,这就被称为局部性原理。数组

时间局部性(Temporal Locality):若是一个信息项正在被访问,那么在近期它极可能还会被再次访问。

好比循环、递归、方法的反复调用等。缓存

空间局部性(Spatial Locality):若是一个存储器的位置被引用,那么未来他附近的位置也会被引用。

好比顺序执行的代码、连续建立的两个对象、数组等。性能

带有高速缓存的 CPU 执行计算的流程

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到 CPU 的高速缓存
  3. CPU 执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

目前流行的多级缓存结构

因为 CPU 的运算速度超越了 1 级缓存的数据 I\O 能力,CPU 厂商又引入了多级的缓存结构。优化

多级缓存结构spa

多核 CPU 多级缓存一致性协议 MESI

多核 CPU 的状况下有多个一级缓存,如何保证缓存内部数据的一致, 不让系统数据混乱。这里就引出了一个一致性的协议 MESI。设计

MESI 协议缓存状态

MESI 是指 4 中状态的首字母。每一个 Cache line 有 4 个状态,可用 2 个 bit 表示,它们分别是:code

缓存行(Cache line): 缓存存储数据的单元。
状态 描述 监放任务
M 修改 (Modified) 该 Cache line 有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中。 缓存行必须时刻监听全部试图读该缓存行相对就主存的操做,这种操做必须在缓存将该缓存行写回主存并将状态变成 S(共享)状态以前被延迟执行。
E 独享、互斥 (Exclusive) 该 Cache line 有效,数据和内存中的数据一致,数据只存在于本 Cache 中。 缓存行也必须监听其它缓存读主存中该缓存行的操做,一旦有这种操做,该缓存行须要变成 S(共享)状态。
S 共享 (Shared) 该 Cache line 有效,数据和内存中的数据一致,数据存在于不少 Cache 中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效 (Invalid) 该 Cache line 无效。
注意: 对于 M 和 E 状态而言老是精确的,他们在和该缓存行的真正状态是一致的,而 S 状态多是非一致的。若是一个缓存将处于 S 状态的缓存行做废了,而另外一个缓存实际上可能已经独享了该缓存行,可是该缓存却不会将该缓存行升迁为 E 状态,这是由于其它缓存不会广播他们做废掉该缓存行的通知,一样因为缓存并无保存该缓存行的 copy 的数量,所以(即便有这种通知)也没有办法肯定本身是否已经独享了该缓存行。 从上面的意义看来 E 状态是一种投机性的优化:若是一个 CPU 想修改一个处于 S 状态的缓存行,总线事务须要将全部该缓存行的 copy 变成 invalid 状态,而修改 E 状态的缓存不须要使用总线事务。 ### MESI 状态转换 理解该图的前置说明: 1. 触发事件
触发事件 描述
本地读取(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 状态(修改) 本地 cache:M
触发 cache:M
其余 cache:I
本地 cache:M
触发 cache:M
其余 cache:I
本地 cache:M→E→S
触发 cache:I→S
其余 cache:I→S
同步主内存后修改成 E 独享, 同步触发、其余 cache 后本地、触发、其余 cache 修改成 S 共享
本地 cache:M→E→S→I
触发 cache:I→S→E→M
其余 cache:I→S→I
同步和读取同样, 同步完成后触发 cache 改成 M,本地、其余 cache 改成 I
E 状态(独享) 本地 cache:E
触发 cache:E
其余 cache:I
本地 cache:E→M
触发 cache:E→M
其余 cache:I
本地 cache 变动为 M, 其余 cache 状态应当是 I(无效)
本地 cache:E→S
触发 cache:I→S
其余 cache:I→S
当其余 cache 要读取该数据时,其余、触发、本地 cache 都被设置为 S(共享)
本地 cache:E→S→I
触发 cache:I→S→E→M
其余 cache:I→S→I
当触发 cache 修改本地 cache 独享数据时时,将本地、触发、其余 cache 修改成 S 共享. 而后触发 cache 修改成独享,其余、本地 cache 修改成 I(无效),触发 cache 再修改成 M
S 状态 (共享) 本地 cache:S
触发 cache:S
其余 cache:S
本地 cache:S→E→M
触发 cache:S→E→M
其余 cache:S→I
当本地 cache 修改时,将本地 cache 修改成 E, 其余 cache 修改成 I, 而后再将本地 cache 为 M 状态
本地 cache:S
触发 cache:S
其余 cache:S
本地 cache:S→I
触发 cache:S→E→M
其余 cache:S→I
当触发 cache 要修改本地共享数据时,触发 cache 修改成 E(独享), 本地、其余 cache 修改成 I(无效), 触发 cache 再次修改成 M(修改)
I 状态(无效) 本地 cache:I→S 或者 I→E
触发 cache:I→S 或者 I →E
其余 cache:E、M、I→S、I
本地、触发 cache 将从 I 无效修改成 S 共享或者 E 独享,其余 cache 将从 E、M、I 变为 S 或者 I
本地 cache:I→S→E→M
触发 cache:I→S→E→M
其余 cache:M、E、S→S→I
既然是本 cache 是 I,其余 cache 操做与它无关 既然是本 cache 是 I,其余 cache 操做与它无关
下图示意了,当一个 cache line 的调整的状态的时候,另一个 cache line 须要调整的状态。
M E S I
M × × ×
E × × ×
S × ×
I
举个栗子来讲: 假设 cache 1 中有一个变量 x = 0 的 cache line 处于 S 状态 (共享)。 那么其余拥有 x 变量的 cache 二、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 都会等待全部缓存响应完成。可能出现的阻塞都会致使各类各样的性能问题和稳定性问题。 ### CPU 切换状态阻塞解决 - 存储缓存(Store Bufferes) 好比你须要修改本地缓存中的一条信息,那么你必须将 I(无效)状态通知到其余拥有该缓存数据的 CPU 缓存中,而且等待确认。等待确认的过程会阻塞处理器,这会下降处理器的性能。应为这个等待远远比一个指令的执行时间长的多。 #### Store Bufferes 为了不这种 CPU 运算能力的浪费,Store Bufferes 被引入使用。处理器把它想要写入到主存的值写到缓存,而后继续去处理其余事情。当全部失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。 这么作有两个风险 #### Store Bufferes 的风险 第1、就是处理器会尝试从存储缓存(Store buffer)中读取值,但它尚未进行提交。这个的解决方案称为 Store Forwarding,它使得加载的时候,若是存储缓存中存在,则进行返回。 第2、保存何时会完成,这个并无任何保证。 ` 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)并非无穷大的,因此处理器有时须要等待失效确认的返回。这两个操做都会使得性能大幅下降。为了应付这种状况,引入了失效队列。它们的约定以下: * 对于全部的收到的 Invalidate 请求,Invalidate Acknowlege 消息必须马上发送 * Invalidate 并不真正执行,而是被放在一个特殊的队列中,在方便的时候才会去执行。 * 处理器不会发送任何消息给所处理的缓存条目,直到它处理 Invalidate。 即使是这样处理器已然不知道何时优化是容许的,而何时并不容许。 干脆处理器将这个任务丢给了写代码的人。这就是内存屏障(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; } ` ##### 引用文章 http://www.importnew.com/1058... https://www.cnblogs.com/yanlo...
相关文章
相关标签/搜索