内存屏障由来及实现思路

 更新时间:2022年1月30日 21:04  点击:207 作者:硬核子牙

很多人知道内存屏障这个东西应该是在学习volatile时看到的,但是对内存屏障依然存在很多疑惑:为什么要加内存屏障?内存屏障能解决什么问题?为什么能解决这些问题?

最近在研究volatile的过程中发现内存屏障这东西如果不搞明白,Java中的volatile就别想学透,所以花了较长时间来研究这块。看了很多资料,写了很多代码测试,这篇文章就来总结下我目前认知中的内存屏障。

01 CPU缓存

如果你不了解讲内存屏障为什么要讲CPU缓存,接着往后看。

学过《计算机组成原理》的同学应该都听过一个词:时钟周期。什么是时钟周期呢?通俗点来讲就是CPU完成一个基本动作需要的时间周期。对硬件有点认识的同学都知道看CPU好不好一定要看的一个参数:多少多少GHZ。这个GHZ跟时钟周期之间是存在一定的换算关系的,感兴趣的同学可以去自行研究。说明一下:不了解这层换算关系不影响你看后面的内容,只要你对时钟周期有一个基本认知就可以了。

在很早以前,CPU里面是没有缓存这块区域的,就是CPU直接读写内存。那后面为什么在CPU中增加了缓存呢?因为CPU的运行效率与读写内存的效率存在着巨大的鸿沟,在读写内存过程中带来的等待浪费了很大的CPU算力。现在最新的内存是DDR4规格,但是向内存中写入数据,据权威资料,需要107个CPU时钟周期,即CPU的运行效率是写内存的107倍。如果CPU只执行写操作需要一个时钟周期,那CPU等待这个写完成需要等待106个时钟周期,是不是很浪费CPU算力?那如何解决呢?就跟我们工作中发现MySQL出现读写瓶颈如何解决是一样的思维:加缓存。

拿我们今天主流的CPU架构来说,现在的CPU主要采用三层缓存:

  • L1、L2缓存成为本地核心内缓存,即一个核一个。如果你的机器是4核,那就是有4个L1+4个L2

  • L3缓存是所有核共享的。即不管你的CPU是几核,这个CPU中只有一个L3

  • L1缓存的大小是64K,即32K指令缓存+32K数据缓存。L2是256K,L3是2M。这不是绝对的,目前Intel CPU基本是这样的设计

这里还补充一个知识点:缓存行(Cache-line)。缓存行是CPU缓存存储数据的最小单位,大小为64B。这块如果展开来讲要讲很久很久,本篇文章就不展开讲了,有兴趣的同学可以自行研究。如果你没有学习过计算机硬件相关知识,可能看不懂。

根据哲学的矛盾相对论:任何问题的解决方案都是一个利与弊共存的矛盾体。加缓存的确有效提升了CPU的执行效率,但是CPU缓存间的数据一致性、CPU缓存与内存间的数据一致性就是不得不去思考与解决的问题了。而且还得保证解决这两层数据一致性的效率要高于不加缓存前浪费的CPU算力,不然这个方案就是一套伪方案:听起来高大上,不解决问题。

02 缓存的一致性

MESI协议就是为了保证CPU各核的缓存、内存间的数据一致性而生的,没有了解过的可以百度普及一下,这个比较简单。这里拓展两点:

一、CPU运算单元与L1缓存间为什么要增加buffer?CPU实现各个核的缓存与内存间的数据一致性的思路有点像socket的三次握手:CPU0修改了某个数据,需要广播告诉其他CPU,这时候CPU0进入阻塞状态等待其他CPU修改其缓存中的状态,待其他CPU都修改完状态返回应答消息后才进入运行状态。虽然这个阻塞的时间很短,但是在CPU的世界里就很长了,为了保证这部分阻塞时间也能得到充分利用,于是加入了buffer。将预读信息存储进去,这样CPU解除阻塞后就可以直接从buffer拿出请求处理。

二、MESI协议的实现思路是:如果CPU0修改了某个数据,需要广播给其他CPU,缓存中没有这个数据的CPU丢弃这个广播消息,缓存中有这个数据的CPU监听到这个广播后会将相应的缓存行改为invalid状态,这样CPU在下次读取这个数据的时候发现缓存行失效,就去内存中读取。这里面童鞋们有没有发现一个问题:只要存在数据修改,CPU就需要去内存取数据,那为什么不实现CPU缓存能共享数据呢?这样CPU在下次读取的时候去CPU0的缓存行去读取就可以啦,而且性能更高。现在的CPU也的确实现了这个思路,对应的协议就是:AMD的MOESI、Intel的MESIF。感兴趣的童鞋自己去研究吧。

03 内存屏障的由来

对于CPU的写,目前主流策略有两种:

1、write back:即CPU向内存写数据时,先把真实数据放入store buffer中,待到某个合适的时间点,CPU才会将store buffer中的数据刷到内存中,而且这两个操作是异步的。这在多线程环境中,有些情况下是可以接受的,但是有些情况是不可接受的,为了让程序员有能力根据业务需要达到同步完成,就设计了内存屏障。关于内存屏障,后面会细讲。

2、write through:即CPU向内存写数据时,同步完成写store buffer与内存。

当前CPU大多数采用的是write back策略。可能有童鞋要问了:为什么呢?因为大多数情况下,CPU异步完成写内存产生的部分延迟是可以接受的,而且这个延迟极短。只有在多线程环境下需要严格保证内存可见等极少数特殊情况下才需要保证CPU的写在外界看来是同步完成的,需要借助CPU提供的内存屏障实现。如果直接采用策略2:write through,那每次写内存都需要等待数据刷入内存,极大影响了CPU的执行效率。

04 内存屏障实现思路

为什么要插入屏障?本质是业务层面不能接受写store buffer与刷回内存这两个异步操作产生的哪怕是极少的延迟,即对内存可见性的要求极高。

内存屏障到底是什么?内存屏障什么都不是,它只是一个抽象概念,就像OOP。如果这样说你不理解,那你把他理解成一堵墙,这堵墙正面与反面的指令无法被CPU乱序执行及这堵墙正面与反面的读写操作需有序执行。

CPU提供了三个汇编指令串行化运行读写指令达到实现保证读写有序性的目的:

SFENCE:在该指令前的写操作必须在该指令后的写操作前完成

LFENCE:在该指令前的读操作必须在该指令后的读操作前完成

MFENCE:在该指令前的读写操作必须在该指令后的读写操作前完成

何谓串行化?你可以理解成CPU把读、写、读写请求放入了一个队列,按照先进先出的顺序执行下去;何谓读操作完成,即CPU执行一次读操作,把值读到了寄存器中;何谓写操作完成,即CPU执行一次写操作,数据刷到内存中了。

到这里就把内存屏障讲清楚了,你悟了吗?

以上就是内存屏障由来及实现思路的详细内容,更多关于内存屏障的资料请关注猪先飞其它相关文章!

原文出处:https://mp.weixin.qq.com/s?__biz=MzIwOTE2MDQyNA==&mid=224748

[!--infotagslink--]

相关文章

  • 浅谈redis key值内存消耗以及性能影响

    这篇文章主要介绍了浅谈redis key值内存消耗以及性能影响,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-07
  • 详解分析MySQL8.0的内存消耗

    这篇文章主要介绍了详解分析MySQL8.0的内存消耗,帮助大家更好的理解和学习使用MySQL,感兴趣的朋友可以了解下...2021-03-23
  • 解决使用OpenCV中的imread()内存报错问题

    这篇文章主要介绍了解决使用OpenCV中的imread()内存报错问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-16
  • 解决tensorflow训练时内存持续增加并占满的问题

    今天小编就为大家分享一篇解决tensorflow训练时内存持续增加并占满的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-04-22
  • 分享C#操作内存读写方法的主要实现代码

    这篇文章介绍了C#操作内存读写方法的主要实现代码,下面让我们来看看具体的实例实现,有需要的朋友可以参考一下...2020-06-25
  • C#托管内存与非托管内存之间的转换的实例讲解

    在本篇文章里小编给大家整理了关于C#托管内存与非托管内存之间的转换的实例以及相关知识点,需要的朋友们学习下。...2020-06-25
  • Redis swap空间(虚拟内存)的使用详解

    这篇文章主要介绍了Redis swap空间的使用示例,帮助大家更好的理解和学习使用Redis数据库,感兴趣的朋友可以了解下...2021-03-25
  • c# 通过内存映射实现文件共享内存的示例代码

    这篇文章主要介绍了c# 通过内存映射实现文件共享内存的示例代码,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下...2021-04-24
  • Android内存溢出及内存泄漏原因进行

    这篇文章主要介绍了Android内存溢出及内存泄漏原因进行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-08-06
  • 完美解决TensorFlow和Keras大数据量内存溢出的问题

    这篇文章主要介绍了完美解决TensorFlow和Keras大数据量内存溢出的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-07-04
  • Redis fork进程分配不到内存解决方案

    这篇文章主要介绍了Redis fork进程分配不到内存解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2021-01-15
  • java应用开发之JVM运行时内存分析

    这篇文章主要介绍了java应用开发之JVM运行时内存,文中附含图文示例内容分析非常简要,有需要的朋友可以借鉴参考下,希望能够有所帮助...2021-09-16
  • 对比C语言中memccpy()函数和memcpy()函数的用法

    这篇文章主要介绍了对比C语言中memccpy()函数和memcpy()函数的用法,二者都是用于复制内存内容,注意区别,需要的朋友可以参考下...2020-04-25
  • DWR内存兼容及无法调用问题解决方案

    这篇文章主要介绍了DWR内存兼容及无法调用问题解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-10-17
  • vivo x9总提示内存不足安装软件失败怎么办?

    vivo x9老是提示内存不足应该怎么处理?下面小编整理了解决办法,一起来看一看吧。 问:vivo x9总提示内存不足安装软件失败怎么办?答:内存不足一般有两种,一个是运行内存...2017-01-22
  • eMMC5.1和UFS2.0两种材质的手机内存哪个更好?有什么区别?

    2016年,安卓手机配置再次提升到了一个新的档次,骁龙820、4GB大内存,存储也飙到了128GB,而在这里,有一个很小的细节,那就是采用了UFS2.0闪存,而不是之前比较流行的eMMC5.1,那么eMMC5.1和UFS2.0两种材质的手机内存哪个更好?有什么区别?下面我们一起来了解下吧...2016-09-12
  • c++对象内存布局示例详解

    C++类的内存布局跟结构体有点像,实际上,类中成员变量的内存布局规则跟结构体是一样的,区别在于函数,虚函数的放置,下面这篇文章主要给大家介绍了关于c++对象内存布局的相关资料,需要的朋友可以参考下...2021-10-24
  • 深入学习C语言中memset()函数的用法

    这篇文章主要介绍了深入学习C语言中memset()函数的用法,是C语言入门学习中的基础知识,需要的朋友可以参考下...2020-04-25
  • sprintboot使用spring-security包,缓存内存与redis共存方式

    这篇文章主要介绍了sprintboot使用spring-security包,缓存内存与redis共存方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-13
  • Python StringIO如何在内存中读写str

    这篇文章主要介绍了python StringIO如何在内存中读写str,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-04-29