CPU 缓存一致性协议 MESI

Wesley13
• 阅读 823

CPU 高速缓存(Cache Memory)

CPU 为何要有高速缓存

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

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

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

比如循环、递归、方法的反复调用等。

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

比如顺序执行的代码、连续创建的两个对象、数组等。

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

  1. 程序以及数据被加载到主内存

  2. 指令和数据被加载到 CPU 的高速缓存

  3. CPU 执行指令,把结果写到高速缓存

  4. 高速缓存中的数据写回主内存

CPU 缓存一致性协议 MESI

目前流行的多级缓存结构

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

多级缓存结构

CPU 缓存一致性协议 MESI

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

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

MESI 协议缓存状态

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

缓存行(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 状态转换

CPU 缓存一致性协议 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 需要调整的状态。

MESI
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 缓存一致性协议 MESI

单核读取

那么执行流程是:
CPU A 发出了一条指令,从主内存中读取 x。
从主内存通过 bus 读取到缓存中(远端读取 Remote read), 这是该 Cache line 修改为 E 状态(独享).
CPU 缓存一致性协议 MESI

双核读取

那么执行流程是:
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 缓存一致性协议 MESI

修改数据

那么执行流程是:
CPU A 计算完成后发指令需要修改 x.
CPU A 将 x 设置为 M 状态(修改)并通知缓存了 x 的 CPU B, CPU B 将本地 cache b 中的 x 设置为 I 状态 (无效)
CPU A 对 x 进行赋值。
CPU 缓存一致性协议 MESI

同步数据

那么执行流程是:

CPU B 发出了要读取 x 的指令。
CPU B 通知 CPU A,CPU A 将修改后的数据同步到主内存时 cache a 修改为 E(独享)
CPU A 同步 CPU B 的 x, 将 cache a 和同步后 cache b 中的 x 设置为 S 状态(共享)。
CPU 缓存一致性协议 MESI

MESI 优化和他们引入的问题

缓存的一致性消息传递是要时间的,这就使其切换时会产生延迟。当一个缓存被切换状态时其他缓存收到消息完成各自的切换并且发出回应消息这么一长串的时间中 CPU 都会等待所有缓存响应完成。可能出现的阻塞都会导致各种各样的性能问题和稳定性问题。

CPU 切换状态阻塞解决 - 存储缓存(Store Bufferes)

比如你需要修改本地缓存中的一条信息,那么你必须将 I(无效)状态通知到其他拥有该缓存数据的 CPU 缓存中,并且等待确认。等待确认的过程会阻塞处理器,这会降低处理器的性能。应为这个等待远远比一个指令的执行时间长的多。

Store Bufferes

为了避免这种 CPU 运算能力的浪费,Store Bufferes 被引入使用。处理器把它想要写入到主存的值写到缓存,然后继续去处理其他事情。当所有失效确认(Invalidate Acknowledge)都接收到时,数据才会最终被提交。
这么做有两个风险

Store Bufferes 的风险

第一、就是处理器会尝试从存储缓存(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)并不是无穷大的,所以处理器有时需要等待失效确认的返回。这两个操作都会使得性能大幅降低。为了应付这种情况,引入了失效队列。它们的约定如下:

  • 对于所有的收到的 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/10589.html https://www.cnblogs.com/yanlong300/p/8986041.html

CPU 缓存一致性协议 MESI

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
CPU高速缓存与极性代码设计
摘要:CPU内置少量的高速缓存的重要性不言而喻,在体积、成本、效率等因素下产生了当今用到的计算机的存储结构。1.介绍2.cpu缓存的结构3.缓存的存取与一致4.代码设计的考量5.最后CPU频率太快,其处理速度远快于存储介质的读写。因此,导致CPU资源的浪费,需要有效解决I
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这