目录

CPU缓存一致性

MESI 协议

MESI优化带来的性能问题

处理器执行时的乱序优化引发的问题

as-if-serial规则

happens-before规则

as-if-serial规则和happens-before规则的区别

Memory Barriers 内存屏障,解决乱序优化问题


CPU缓存一致性

要解决CPU缓存一致性这一问题,就需要一种机制,来同步两个不同核心里面的缓存数据。要实现的这个机制的话,要保证做到下面这 2 点:

  • 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(*Wreite Propagation*)
  • 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串形化(*Transaction Serialization*)

要实现事务串形化,要做到 2 点:

  • CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心;
  • 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。

要实现写传播,要做到当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常⻅实 现的方式是总线嗅探(Bus Snooping)。

还是以前面的 i 变量例子来说明总线嗅探的工作机制,当 A 号 CPU 核心修改了 L1 Cache 中 i 变量的 值,通过总线把这个事件广播通知给其他所有的核心,然后每个 CPU 核心都会监听总线上的广播事件,并 检查是否有相同的数据在自己的 L1 Cache 里面,如果 B 号 CPU 核心的 L1 Cache 中有该数据,那么也需 要把该数据更新到自己的 L1 Cache。

可以发现,总线嗅探方法很简单 CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这无疑会加重总线的负载。

另外,总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并 不能保证事务串形化。

于是,有一个协议基于总线嗅探机制实现了事务串形化,也用状态机机制降低了总线带宽压力,这个协议 就是 MESI 协议,这个协议就做到了 CPU 缓存一致性。

MESI 协议

MESI 协议其实是 4 个状态单词的开头字母缩写,分别是:

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

这四个状态来标记 Cache Line缓存行的不同状态。

「已修改」状态就是前面提到的脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存里。而「已失效」状态,表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。

「独占」和「共享」状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的。

「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

另外,在「独占」状态下的数据,如果有其他核心从内存读取了相同的数据到各自的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态。

那么,「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。

事实上,整个 MESI 的状态可以用一个有限状态机来表示状态流转。还有一点,对于不同状态触发的事件操作,可能是来自本地 CPU 核心发出的广播事件,也可以是来自其他 CPU 核心通过总线发出的广播事件。下图即是 MESI 协议的状态图:

下面的四个状态变化,指的是,当前CPU某个核中的指定 Cache Line的状态变化!

MESI 协议的四种状态之间的流转过程,汇总成了下面的表格,可以更详细的看到每个状态转换的原因:

Local Read:当前核读自己

Local Write:当前核写自己

Remote Read:其他核来读

Remote Write:其他核来写

cpu cache数据的写入内存

事实上,数据不止有读取还有写入,CPU对数据进行一系列操作后,

将数据写入CPU的cache,内存和cache的数据就不同了,也叫脏了Dirty,终究是要需要把cache同步到内存中。

问题的关键就在于在什么时机去把数据写到内存?

一般来讲有以下两种策略

写直达(Write Through)

保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中,这种方法称为写直达 。

写直达法很直观,也很简单,但是问题明显,无论数据在不在 Cache 里面,每次写操作都会写回到内存, 这样写操作将会花费大量的时间,无疑性能会受到很大的影响。

写回(Write Back写回)

由于写直达的机制会有性能问题,所以产生了写回(Write Back)的方法

在写会机制中,当发生写操作时,新的数据仅仅被写入 Cache Block 里,只有指定某些场景才需要写到内存中,减少了数据写回内存的频率,这样便可以提高系统的性能。

在MESI协议中,需要把CPU内核中缓存数据刷到内存的场景:

想要Read 或者 Write 位于其他核心处于Modified的数据

1、A核心的Cache数据处于Invalid失效状态,A想要读取的数据位于B核心,并且B核心的状态是Modified已修改的时候,需要将B的Cache Line数据刷到内存
2、A核心Cache数据处于Modified已修改状态,B核心对缓存来 Read 和 Write,都需要A核心先把缓存数据刷到内存中去。

MESI优化带来的性能问题

各个 CPU 缓存行的状态是通过消息传递来进行的。如果CPU0要对一个在缓存中共享的变量(S状态)进行写入,首先需要发送一个失效的消息给到其他缓存了该数据的 CPU。并且要等到他们的确认回执,CPU0 在这段时间内都会处于阻塞状态。这对于频率很高的CPU来说,简直不能接受。执行过程如下

Store Buffer

为了解决此问题,cpu层面引入了Store Buffer。此时的执行过程为:

CPU0只需要在写入共享数据(独占数据不用)时,

1、发送 invalidate 消息给其他核

2、直接把数据写入到store buffer 中,同时,(然后继续去处理其他指令,因为数据还没刷到cache里面,就去搞其他事情,这个就是处理器执行时的乱序优化

3、收到其他所有CPU发送了invalidate acknowledge消息时,再将 store buffer 中的数据数据存储至 cache line中。最后再从缓存行同步到主内存。

另外一个CPU1核收到Invalid消息时,

1、会把消息写入自身的Invalidate Queue中,

2、随后异步将其设为Invalid状态。

和Store Buffer不同的是,当前CPU1核心使用Cache时并不扫描Invalidate Queue部分,所以可能会有极短时间的脏读问题。这里的Store Buffer和Invalidate Queue的说法是针对一般的SMP架构来说的,不涉及具体架构。

Store-buffer Forwarding

引入store buffer之后又带了新的问题,同一个 CPU 在顺序执行指令的过程中,有可能出现,前面的已经执行写入变更,但对后面的代码逻辑不可见

a=1
b=a+1
assert(a==2)
复制代码

cpu对a赋值为1,此时a变量进入到store buffer,缓存中的a还是等于0,此时执行b=a+1得到的结果是b=1,assert不通过。

解决方案就是采用Store Forwarding

对于同一个 CPU 而言,在读取 a 变量的时候,如若发现 Store Buffer中有尚未写入到缓存的数据 a,则直接从 Store Buffer 中读取。这就保证了,逻辑上代码执行顺序,也保证了可见性。

处理器执行时的乱序优化引发的问题

通过 Store-Buffer Forwarding 解决了单个 CPU 执行顺序性和内存可见性问题,但是在全局多 CPU 的环境下,这种内存可见性恐怕就很难保证了。

a = 0;
isFinish = false;
void cpu0run()
{a = 1;isFinish = true;// 其他操作
}void cpu1run()
{while (isFinish) continue;assert(a == 1);// 其他操作
}

假设上面的 cpu0run方法被CPU0执行,cpu1run方法被CPU1执行,也就是我们常说的多线程环境。试想,即便在多线程环境下,cpu0run 和 cpu1run 如若严格按照理想的顺序执行,是无论如何都不会出现 assert failed 的情况的。

但是有概率会出现了 assert failed ,来分析整个过程,假设 a初始值为0,isfinsh为false,a被CPU0和CPU1共同持有(S状态),isfinsh被CPU0 独占(E状态);

CPU 0 处理 a=1 之前发送 Invalidate 消息给 CPU1 ,并将其放入 Store Buffer ,尚未及时刷入缓存;

CPU 0 转而处理 isFinish = true ,由于isFinish是独占锁E,此时 isfinsh = true 直接被刷入缓存; CPU 1 发出 Read 消息读取 isfinsh 的值,发现 isfinsh = true ,跳出 while 语句;

CPU 1 发出 Read 消息读取 a 的值,发现 a 却为旧值 0,assert failed。

在日常开发过程中也是完全有可能遇到上面的情况,由于 a 的变更对 CPU1 不可见,虽然执行指令的时序没有真正被打乱,但对于 CPU1 来说,这造成了

isfinsh = true 先于 a=1 执行的假象,这种看是乱序的问题,通常称为 “重排序”。当然上面所说的情况,只是指令重排序的一种可能。

as-if-serial规则

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。 编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。 但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。示例代码如下:

int a=1;
int b=2;
int c=a+b;

a和c之间存在数据依赖关系,同时b和c之间也存在数据依赖关系。因此在最终执行的指令序列中,c不能被重排序到A和B的前面(c排到a和b的前面,程序的结果将会被改变)。但a和b之间没有数据依赖关系,编译器和处理器可以重排序a和b之间的执行顺序。

happens-before规则

Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。

  • 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
  • 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
  • volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
  • 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
  • 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
  • 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
  • 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
  • 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C

代码demo : https://blog.csdn.net/Sword52888/article/details/126079993

as-if-serial规则和happens-before规则的区别

  1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。
  2. as-if-serial语义给编写单线程程序的程序员创造了一个幻觉:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻觉:正确同步的多线程程序是按happens-before指定的顺序来执行的。
  3. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

Memory Barriers 内存屏障,解决乱序优化问题

CPU级别内存屏障其作用有两个:

  1. 保证特定操作的执行顺序,防止指令之间的重排序
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

指令重排中Load和Store两种操作会有Load-Store、Store-Load、Load-Load、Store-Store这四种可能的乱序结果。

从硬件层面很难去知道软件层面上的这种前后依赖关系,所以没有办法通过某种手段自动去解决。所以在 CPU 层面提供了 memory barrier(内存屏障)的指令,从硬件层面来看这个 memroy barrier 就是 CPU flush store bufferes 中的指令。软件层面可以决定在适当的地方来插入内存屏障。

什么是内存屏障?

从前面的内容基本能有一个初步的猜想,内存屏障就是将 store bufferes 中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。

X86 的 memory barrier 指令包括 lfence(读屏障) sfence(写屏障) mfence(全屏障)

  • sfence ,实现Store Barrior(写屏障) 会将store buffer中缓存的修改刷入L1 cache中,(我的理解,就是必须等到其他CPU核心的Invalid ack之后,再flush到L1 cache中,然后再继续往下走)使得其他cpu核可以观察到这些修改,而且之后的写操作不会被调度到之前,即sfence之前的写操作一定在sfence完成且全局可见;
  • lfence ,实现Load Barrior(读屏障) 会将invalidate queue失效,强制读取入L1 cache中,(上面说了有几率出现 脏读)而且lfence之后的读操作不会被调度到之前,即lfence之前的读操作一定在lfence完成(并未规定全局可见性);
  • mfence ,实现Full Barriorr(全屏障) 同时刷新store buffer和invalidate queue,保证了mfence前后的读写操作的顺序,同时要求mfence之后写操作结果全局可见之前,mfence之前写操作结果全局可见;
  • lock 用来修饰当前指令操作的内存只能由当前CPU使用,若指令不操作内存仍然由用,因为这个修饰会让指令操作本身原子化,而且自带Full Barrior效果;还有指令比如IO操作的指令、exch等原子交换的指令,任何带有lock前缀的指令以及CPUID等指令都有内存屏障的作用。

有了内存屏障以后,对于上面这个例子,我们可以这么来改,从而避免出现可见性问题。

a = 0;
isfinsh = false;
void cpu0run()
{a = 1;// 写屏障,上面a=1,收到其他核心的Invalid ack之后,(此时其他核的数据a放在队列Invalid queue中)再从store buffer刷到L1 cache之后,再往下操作Store Memory Barrier();isfinsh = true;// 其他操作
}void cpu1run()
{while (isfinsh) continue;Load Memory Barrier();// 读屏障,使得CPU1把Invalid queue中的失效数据刷到L1 cache中,此时发现a已经失效,那么去主存读。 由于是CPU0的a数据是Modified状态,所以会让cpu1把a刷到主存中去assert(a == 1);// 其他操作
}

CPU中的MESI协议(Intel)相关推荐

  1. 2021-04-04 CPU缓存一致性 MESI协议

    一 CPU以及缓存和高速缓存结构 1.1 CPU结构 我们知道CPU主要功能,一是控制,一是运算.主要包括寄存器.控制单元.运算单元和中断系统,主要架构如下: 控制单元:主要负责分析和解释指令 算数逻 ...

  2. CPU 缓存一致性 MESI 协议

    为什么需要缓存一致 目前主流电脑的 CPU 都是多核心的,多核心的有点就是在不能提升 CPU 主频后,通过增加核心来提升 CPU 吞吐量.每个核心都有自己的 L1 Cache 和 L2 Cache,只 ...

  3. volatile可见性MESI协议volatile

    volatile 的作用 volatile 的主要作用有三点: - 保证变量的内存可见性 ,有序性(禁止指令重排序),不保证原子性. 可见性 简单解释:指当多个线程访问同一个变量时,一个线程修改了这个 ...

  4. 12 张图看懂 CPU 缓存一致性与 MESI 协议,真的一致吗?

    本文已收录到  GitHub · AndroidFamily,有 Android 进阶知识体系,欢迎 Star.技术和职场问题,请关注公众号 [彭旭锐] 进 Android 面试交流群. 前言 大家好 ...

  5. 缓存一致性协议和CPU缓存架构(MESI协议)、伪共享

    目录 简介 CPU高速缓存 为什么要有CPU高速缓存 局部性原理 缓存一致性 缓存一致性的要求 总线窥探 工作原理 窥探协议 一致性协议 MESI协议 总线事务 总线仲裁 总线锁定 缓存锁定 伪共享问 ...

  6. Java内存模型MESI协议

    参考链接 也许,这是东半球最叼的Java内存模型 CPU缓存一致性协议MESI 目录 多线程并发编程的三个特性实现 缓存的出现 缓存不一致 MESI协议 MESI优化和他们引入的问题 硬件内存模型 v ...

  7. mesi协议怎么实现_volatile的底层实现原理

    volatile关键字有两个作用 保证被volatile修饰的共享变量(vlatile int a=1)对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以 ...

  8. 【原理/Java并发】从volatile到MESI协议

    文章目录 1 前言 2 有序性 2.1 编译器层面的内存屏障 2.2 CPU层面的内存屏障 3 可见性 3.1 MESI协议 3.2 Store Buffer 和 Invalid Queue 3.3 ...

  9. 北桥(龙芯的北桥主要是amd的 没有内存控制器 内存控制器在龙芯cpu中)

    简介 随着"龙芯"等拥有完全自主产权CPU的诞生,我国结束了无"芯"的历史.但这还不够,因为要构成一个完整的拥有自主产权的计算机系统,还必须有操作系统,芯片组和 ...

最新文章

  1. Linux下截取指定时间段日志并输出到指定文件
  2. Py之ipython:Python库之ipython的简介、安装、使用方法详细攻略
  3. SyncStudy Poster
  4. 51NOD 1138 连续整数的和
  5. [BZOJ4259]残缺的字符串
  6. service mesh 数据平面nginmesh
  7. python 合并word文件_python自动化办公(1)—— 批量合并word文档
  8. ListView的headerView下拉刷新PullToZoomInListView分析
  9. 提高软件开发工作效率的几种方法
  10. 拭血长短句手札【2013-2017】微信公众号 shixuemp
  11. 纯JS省市区三级联动(行政区划代码更新至2015-9-30)
  12. Keil中文显示设置
  13. geany配置python_Geany配置python教程解析
  14. 谷歌浏览器模拟微信/QQ内置浏览器调试及js判断方法
  15. 3月二手住宅市场缓慢回温
  16. 用单片机c51电子秤的c语言,基于51单片机的电子秤系统设计
  17. 最近疯狂的爱上了功放
  18. java语言生成plist下载ipa文件
  19. 4种常见的缓存模式,你都知道吗?
  20. java--(三)类与对象

热门文章

  1. java 123456转换成abcdef_java 数字与字母的转换 (转)
  2. Python基础-DAY16
  3. 用秦九昭公式计算多项式
  4. abaqus切削为什么没有切屑_Abaqus在金属切削方面的实例
  5. 学原油期货买什么书(怎么样买原油期货)
  6. 聊聊rel=external nofollow和rel=noopener noreferrer
  7. 单位换算 M、Mb、MB
  8. 【深度学习前沿应用】目标检测
  9. java对文件进行压缩的两种方法
  10. 杭电oj(java)1091