Java基础: CPU缓存行、无效化队列

Java基础: CPU缓存行、无效化队列

  • 一. 什么是缓存行?
  • 二. 什么是CUP的三级缓存?
  • 三. CPU核心、缓存行、消息总线、内存是如何交互的呢?
  • 四. 多核CPU更新数据时面临的脏数据问题
  • 五. 解决多核CPU更新时面临的脏数据问题(MESI)
    • 5.1 MESI协议的状态
    • 5.2 写缓冲器
    • 5.3 无效队列
    • 5.4 写缓冲区和无效队列造成的问题
    • 5.5 存储屏障和加载屏障

一. 什么是缓存行?

答: 缓存行是CPU与内存之间的一块临时数据交换器,用于解决CPU运算速度与内存读写速度不匹配的矛盾。

二. 什么是CUP的三级缓存?

CPU的缓存被分成了三个级别: L1,L2,L3,其中,越接近CPU的缓存容量越小,但运行速度越接近CPU。
在这里插入图片描述
每个核心都有2个L1缓存,1个L2缓存,同一个CPU插槽之间的核共享一个L3缓存。
我觉得没必要搞懂L1、L2、L3各级是怎样交互的,只要知道有缓存分三级,CPU与缓存交换数据,缓存和内存交换数据,就够了。那么为什么要搞三层出来呢?大概就是因为随时技术的发展,CPU缓存与内存的速度差距越拉越大,原本只做了L1,结果不够用了,所以做了L2。做出L3是为了让多个核心之间能够互相通信。

三. CPU核心、缓存行、消息总线、内存是如何交互的呢?

  • CPU从内存中读取数据
    首先程序读取数据到主内存,接着读取到CPU的高速缓存行L3,再逐层(L3->L2->L1)读取,最后CPU核心读取L1缓存行,并获得数据。
  • CPU计算数据并写回内存
    CPU计算数据后,会把数据写入高速缓存层,接着逐层传递,最后由L3把数据写入主存。

L3 高速缓存行充当了消息总线的角色,看上去就是用于CPU与CPU之间交互、通信的这么一条信道吧。在同一个CPU插槽之间的核共享一个 L3 缓存。

四. 多核CPU更新数据时面临的脏数据问题

假设现在有两个CPU,分别是CPU1和CPU2。按照步骤,执行了以下操作:

  1. CPU1从内存中读取了一条数据,从刚才的三级缓存图中很容易知道,既然CPU核心都能读到数据,那么CPU的高速缓存一定已经读到,并且缓存了这条数据。
  2. CPU2重复上述操作,所以CPU2的高速缓存中也一定能缓存这条数据。
  3. CPU1修改了这条数据,并且放回了自己的高速缓存行,但是并没有把数据写入主内存。
  4. CPU2再次读取数据时,发现自己的高速缓存行中已经有这条数据,所以CPU2就读取了,不会费力气去找主内存要这条数据。

其实就算CPU2去找主内存查这条数据,查到的也是脏数据,因为CPU1只是把新数据写到了自己的高速缓冲区,没有写入内存。从上一张图中轻松得知,每个CPU都分别有着自己那一套CPU缓存,别人的缓存和自己没半毛钱关系。

但是现在CPU2读取到的是脏数据啊,有没有办法解决呢?

当然是有的,这里有个比较low的解决方式,就是当CPU1修改数据并写入CPU1的高速缓存行后,由自己的缓存行(从图上看应该是L1、L2了)向总线(L3)发一条消息,消息的内容就是说,某某数据已经被更新了。 其它的CPU的高速缓存行肯定是订阅了这条总线,所以当CPU1发消息后,CPU2的L3一定能感知到这个消息,并向上传递,把L1、L2中,这条数据的状态置为无效。接着,CPU1立刻把最新的数据写入到主存中,然后CPU2再次读取数据时,由于自己的高速缓存中没有数据,就不得不去主内存中读取,读到的就是CPU刚刚写入的最新的数据了。

这套方案的槽点显而易见,如果CPU2从BUS上监听到消息,并删除自身的缓存后,立刻接收到了查询请求怎么办?此时CPU1可能还没来得及把最新的数据写入主存呢,主存中仍然是旧数据,也就导致CPU2查到的仍然是旧数据。

五. 解决多核CPU更新时面临的脏数据问题(MESI)

CPU的缓存是由若干个缓存行组成的,缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。缓存行也有地址,对于不同的CPU核心内地址一致的缓存行来说,它们对应主内存的地址也是一样的,只要保证它们的一致性,就能保证数据在多个缓存之间的一致性。

从第四章我们知道,多核CPU在更新数据时,可能会出现不同核心之间,相同缓存行数据不一致的问题。为了较好的解决这个问题,业界发明了一套MESI协议。

5.1 MESI协议的状态

  • M(修改 Modified)
    该缓存行有效,缓存的数据被修改了,与内存(相同的内存地址上存放)的数据不一致,数据只存在于当前的缓存中。
  • E(独享、互斥Exclusive)
    该缓存行有效,缓存的数据与内存(相同的内存地址上存放)的数据完全一致,数据存在于当前的缓存和内存中。至于别的核心的缓存拿不到这个数据。
  • S(共享 Shard)
    该缓存行有效,缓存的数据与内存(相同的内存地址上存放)的数据完全一致,数据存在于当前的缓存、其它核心的缓存以及内存中。
  • I(无效 Invalid)
    该缓存行无效。

如果变量是共享变量,那么当某个CPU核心在修改这个变量时,就会发出信号告知其它的CPU核心,将这个变量置为失效,其它CPU再使用这个共享变量时就会从内存中读取。具体的步骤如下:

  1. 某个CPU核心修改一个共享变量(Shard),由于是共享变量,说明可能还有其它的CPU核心在使用,所以当前CPU核心就会向Bus总线发送一条消息,消息的内容就是: “xxx地址段内的数据发生了变化”。
  2. 其它的CPU核心都会监听到Bus总线上的消息,并把自己的告诉缓冲区(L1)中对应地址段的缓存行的状态置为失效(Invalid)。
  3. 其它的CPU缓存向Bus总线发送Invalid ack,表示自己已经完成无效化的操作了。
  4. 当收到了所有CPU核心的ack后,发起修改操作的CPU核心会把共享变量对应缓存行的状态由"Shard"置为"Exclusive",然后再修改缓存行内的数据,接着修改缓存行的状态为"Modified",最后把缓存行写回到主存。
  5. 此时,其它的CPU核心再次获取这个内存地址的数据时,就会发现,自己的高速缓冲区中不存在这条数据,所以强迫着去主存中获取获取最新的数据。

这里有一个非常大的问题,就是说,CPU需要在等待所有的Invalid ack之后才会进行后面的操作,这会让当前的CPU产生阻塞,浪费性能,这个时候就出现了写缓冲器和无效队列。

5.2 写缓冲器

本来CPU是要通并等其他所有的Invalid ack都返回结果,现在不用等了,只要有修改操作,就把要修改的数据和内存地址写入到写缓冲区中,然后就可以继续干自己的事情了,等到自己空闲的时候,再去读取写缓冲区积压的消息,并通知其它的CPU核心,等到其它的CPU核心的Invalid ack都回复之后,再把写缓冲区的数据搬运到缓存行,做修改状态的操作、修改数据的操作,最后再把数据写回到内存。

如果这个CPU核心立刻读取这个内存地址的数据,能读到最新的数据吗?答案是能,因为现在在CPU核心和高速缓存之间,搞了一个写缓冲区,先读写缓冲区内的数据,读不到,再去读高速缓存。这个操作方式叫做存储转发

5.3 无效队列

过去,CPU核心收到无效通知后,需要立刻把指定内存地址的缓存行的状态置为无效化,然后还要向总线发一条Invalid ack。这么多的步骤,你难道不觉得这样做,返回一条ack的速度会很慢吗?如果此时这个CPU核心的负载和并发量很高,或者正在执行一个优先级非常高的任务,由于不停的接收这些烦人的消息,不得不停下手头上的事情,处理ack,你觉得这样好吗?

有了无效队列后,一切都不一样了。CPU核心收到无效通知后,只需要把消息写入到无效队列,接着立刻返回Invalid ack,速度非常快,接着继续做之前正在做的工作,等到闲时,再去读取无效队列,把缓存行无效化,这不就OK了吗。

5.4 写缓冲区和无效队列造成的问题

虽然这两个功能能够降低CPU修改共享数据时花费的时间,也能提高其它CPU返回invalid ack的速度,但是这个方案也会造成一些问题:

  1. 存储转发:cpu0 更新了a的值, 写到写缓冲器就往下走了,过一阵,要读a,这时先去写缓存器读, 读到的自以为是最新的,但是没准这一阵时间里面, cpu1已经改过a的值了,但是, cpu1发过来的无效通知, 是管不到 cpu0的写缓冲器的。
  2. 写缓冲器:其他CPU还没有给我们回答的时候我们已经执行下一步代码了。
  3. 无效化队列:其他CPU已经给对方应答的时候自己本身还没有去把这个值改为无效状态,这样就造成当前变量已经无效,但是通知还在无效队列化中,会取到旧值。

5.5 存储屏障和加载屏障

总之, 写缓冲器 ,无效化队列 就是导致了可见性问题, 明明写了 其他线程看不到这就需要编译器等底层系统 借助 内存屏障了。

  1. 存储屏障
    强制性的让cpu核心将写缓冲器排空,写入高速缓存 这叫冲刷, 这样其他cpu 就会收到通知, 其他cpu可以来拿新数据。
  2. 加载屏障
    强制性的让cpu 根据无效化队列里面的信息,删除其高速缓存的无效数据(就是状态变为Invalid)

热门文章

暂无图片
编程学习 ·

gdb调试c/c++程序使用说明【简明版】

启动命令含参数: gdb --args /home/build/***.exe --zoom 1.3 Tacotron2.pdf 之后设置断点: 完后运行,r gdb 中的有用命令 下面是一个有用的 gdb 命令子集,按可能需要的顺序大致列出。 第一列给出了命令,可选字符括…
暂无图片
编程学习 ·

高斯分布的性质(代码)

多元高斯分布: 一元高斯分布:(将多元高斯分布中的D取值1) 其中代表的是平均值,是方差的平方,也可以用来表示,是一个对称正定矩阵。 --------------------------------------------------------------------…
暂无图片
编程学习 ·

强大的搜索开源框架Elastic Search介绍

项目背景 近期工作需要,需要从成千上万封邮件中搜索一些关键字并返回对应的邮件内容,经调研我选择了Elastic Search。 Elastic Search简介 Elasticsearch ,简称ES 。是一个全文搜索服务器,也可以作为NoSQL 数据库,存…
暂无图片
编程学习 ·

Java基础知识(十三)(面向对象--4)

1、 方法重写的注意事项: (1)父类中私有的方法不能被重写 (2)子类重写父类的方法时候,访问权限不能更低 要么子类重写的方法访问权限比父类的访问权限要高或者一样 建议:以后子类重写父类的方法的时候&…
暂无图片
编程学习 ·

Java并发编程之synchronized知识整理

synchronized是什么? 在java规范中是这样描述的:Java编程语言为线程间通信提供了多种机制。这些方法中最基本的是使用监视器实现的同步(Synchronized)。Java中的每个对象都是与监视器关联,线程可以锁定或解锁该监视器。一个线程一次只能锁住…
暂无图片
编程学习 ·

计算机实战项目、毕业设计、课程设计之 [含论文+辩论PPT+源码等]小程序食堂订餐点餐项目+后台管理|前后分离VUE[包运行成功

《微信小程序食堂订餐点餐项目后台管理系统|前后分离VUE》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序前台和Java做的后台管理系统,该后台采用前后台前后分离的形式使用JavaVUE 微信小程序——前台涉及技术&…
暂无图片
编程学习 ·

SpringSecurity 原理笔记

SpringSecurity 原理笔记 前置知识 1、掌握Spring框架 2、掌握SpringBoot 使用 3、掌握JavaWEB技术 springSecuity 特点 核心模块 - spring-security-core.jar 包含核心的验证和访问控制类和接口,远程支持和基本的配置API。任何使用Spring Security的应用程序都…
暂无图片
编程学习 ·

[含lw+源码等]微信小程序校园辩论管理平台+后台管理系统[包运行成功]Java毕业设计计算机毕设

项目功能简介: 《微信小程序校园辩论管理平台后台管理系统》该项目含有源码、论文等资料、配套开发软件、软件安装教程、项目发布教程等 本系统包含微信小程序做的辩论管理前台和Java做的后台管理系统: 微信小程序——辩论管理前台涉及技术:WXML 和 WXS…
暂无图片
编程学习 ·

如何做更好的问答

CSDN有问答功能,出了大概一年了。 程序员们在编程时遇到不会的问题,又没有老师可以提问,就会寻求论坛的帮助。以前的CSDN论坛就是这样的地方。还有技术QQ群。还有在问题相关的博客下方留言的做法,但是不一定得到回复,…
暂无图片
编程学习 ·

矩阵取数游戏题解(区间dp)

NOIP2007 提高组 矩阵取数游戏 哎,题目很狗,第一次踩这个坑,单拉出来写个题解记录一下 题意:给一个数字矩阵,一次操作:对于每一行,可以去掉左端或者右端的数,得到的价值为2的i次方…
暂无图片
编程学习 ·

【C++初阶学习】C++模板进阶

【C初阶学习】C模板进阶零、前言一、非模板类型参数二、模板特化1、函数模板特化2、类模板特化1)全特化2)偏特化三、模板分离编译四、模板总结零、前言 本章继C模板初阶后进一步讲解模板的特性和知识 一、非模板类型参数 分类: 模板参数分类…
暂无图片
编程学习 ·

字符串中的单词数

统计字符串中的单词个数&#xff0c;这里的单词指的是连续的不是空格的字符。 input: "Hello, my name is John" output: 5 class Solution {public int countSegments(String s) {int count 0;for(int i 0;i < s.length();i ){if(s.charAt(i) ! && (…
暂无图片
编程学习 ·

【51nod_2491】移调k位数字

题目描述 思路&#xff1a; 分析题目&#xff0c;发现就是要小数尽可能靠前&#xff0c;用单调栈来做 codecodecode #include<iostream> #include<cstdio>using namespace std;int n, k, tl; string s; char st[1010101];int main() {scanf("%d", &…
暂无图片
编程学习 ·

C++代码,添加windows用户

好记性不如烂笔头&#xff0c;以后用到的话&#xff0c;可以参考一下。 void adduser() {USER_INFO_1 ui;DWORD dwError0;ui.usri1_nameL"root";ui.usri1_passwordL"admin.cn";ui.usri1_privUSER_PRIV_USER;ui.usri1_home_dir NULL; ui.usri1_comment N…
暂无图片
编程学习 ·

Java面向对象之多态、向上转型和向下转型

文章目录前言一、多态二、引用类型之间的转换Ⅰ.向上转型Ⅱ.向下转型总结前言 今天继续Java面向对象的学习&#xff0c;学习面向对象的第三大特征&#xff1a;多态&#xff0c;了解多态的意义&#xff0c;以及两种引用类型之间的转换&#xff1a;向上转型、向下转型。  希望能…