首页 > 系统相关 >内存屏障简介

内存屏障简介

时间:2024-09-11 20:21:07浏览次数:3  
标签:Cache 简介 cache 屏障 内存 data CPU

内存屏障

编译乱序

异步场景中,经常使用多线程一起处理任务并且通过一个共享变量进行状态的共享,如下,function2在请求data数据时通过status判断数据是否就绪,function1准备数据完成后修改status。

bool status = false;
char data[50] = "Hello";
void function1() {
    strcpy(data, "World");
    status = true;
}
void function2() {
    while (status) {
        printf("%s\n", data); 
    }
}

编译会对代码进行优化,编译出更加高效的代码,其中可能会将代码的顺序打乱(能确保单线程情况下功能不会受影响),上述代码中,对于funcction1来说编译器认为data和status是没有依赖关系,依赖关系在function2中体现,只有程序员知道依赖关系,所以编译器可能会打乱顺序成如下

void function1() {
    status = true;
    strcpy(data, "World");
}

当执行status = true;后function2立马进入循环对data进行访问,此时function1并没有对data进行赋值,printf将打印一个非预期的data。

所以编译器通常会提供编译器屏障(又叫优化屏障)。Linux内核提供了函数barrier(),用于让编译器保证其之前的内存访问先于其之后的内存访问完成。避免指令重排。通常不同语言的volatile关键字也有类似的效果。

ps: 单个CPU也有执行乱序的情况但是不会出现问题,一条指令的执行过程可分解为若干阶段,像流水线一样按顺序执行所以叫指令流水,包括取指(IF)、译码/读寄存器(ID)、执行/计算地址(EX)、访存(MEN)、写回(WB)等。当一个指令在取指(IF)阶段时,其他阶段的处理单元都是空闲的,所以为了进一步利用CPU资源,两条无关的指令CPU可能会同时派发到不同的执行单元执行,比如指令1在ID阶段,指令2在IF阶段执行,相当于多个指令并行执行。但在单个CPU上,指令能通过指令队列顺序获取并执行,结果利用队列顺序返回寄存器堆,这使得程序执行时所有的内存访问操作看起来像是按程序代码编写的顺序执行的。所以不会出问题。

Cache和cache一致性协议

CPU需要将一个变量(假设地址是A)加1,一般分为以下3个步骤:

  1. CPU 从主存中读取地址A的数据到内部通用寄存器 x1(arm通用寄存器x0,x1,…x30,wzr)(load)。

  2. 通用寄存器 x1 加1。

  3. CPU 将通用寄存器 x1 的值写入主存(store)。

CPU register的访问速度一般小于1ns,主存的访问速度一般是65ns左右,在上述计算中CPU需要白白等待load/store100ns,导致CPU算力没有被充分使用。而且将主内存速度提高到register一样的访问速度的成本是昂贵不可接受的。因为程序运行具有局部性,即刚刚访问的内存之后会再次访问的概率很大,所以使用缓存(Cache)的思路提高内存的访问速度。通常会设计多级缓存,L1、L2、L3,访问速度依次递减,同时容量依次递增。在多核CPU中,通常L1是每个核独占的,L2是每个相同NUMA节点下共享的,L3是所有核共享的。

有了Cache后访问load/store时先访问cache,cache上没有再去将主存内容加载到cache中再访问。

ps: 类似分页管理机制,cache也是分块管理,每次访问只能固定大小的内存块,称为cache line size,一般cache line的大小是4-128 Byts。另外容量小的cache映射到容量大的主内存方法,有全相联映射、直接映射、组相联映射等多种。这些都是软件无感知的。

有了cache,一份数据分别在cache和主内存中都有一份,cache和主存两份数据如何保证一致性同时又能得到很高的性能?出现多种一致性协议

对于单核CPU,只有一个cache,通常当CPU读数据时,如果cache命中就直接返回,不命中就返回从主内存中读取到cache中之后再读cache返回。有两种更新策略,写直通即每次写都直接刷新到主内存,回写即每次只写cache上,当cache被换出时回写到主内存中。

多核CPU,每个核都有单独的cache,相对于单核CPU还需要考虑多个核的私有cache的一致性问题,主要有2种策略:基于总线监听的一致性策略 和 基于目录的一致性策略

基于总线监听的一致性策略,也叫总线嗅探(Bus Snooping),原理简述如下:

1、当有一个核修改了Cache中的值,会通过总线把这个事件广播给其他所有核;

2、每个核都会去监听总线中的数据广播,并检测自己是否有相同数据的副本在本核的Cache中;如果有副本,就执行相应操作来确保多核的缓存一致性

基于目录的一致性策略会维护一个表格,叫做目录(directory-based),保存在哪些核上有副本及其对应的状态等信息;当CPU执行写操作时,不会再向所有核的Cache进行广播,而是是通过此目录来跟踪所有缓存中数据副本的状态,仅将写信息发送到指定的数据副本中;这样相比总线嗅探节省大量总线流量。

它又分为SI,MSI,MESI策略

MESI 协议将数据表示成四个状态,分别是:

Modified,已修改,数据已经被更新过,但是还没有写到内存里

Exclusive,独占,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据

Shared,共享,数据在多个 CPU 核心的 Cache 里都有

Invalidated,已失效,数据已经失效了,不可以读取该状态的数据

对不同状态的数据进行load/store会触发状态转移,同时触发对应同步机制等

https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm

如果严格按照MESI协议,某核在写入Invalid状态cache时,需要向其他核广播RFO获得独占权,其它核收到消息后,使他们对应的缓存副本失效,并返回 Invalid acknowledgement 消息,直到这个核收到消息才能修改缓存,期间当前核只能空等待,性能变差。所以通过 Store Buffer写缓冲区和 Invalidate Queue失效队列机制来进一步优化性能。

Store Buffer机制是当前核需要修改cache时将相关信息写入Store Buffer中,之后这个核就可以直接返回继续做其他工作无需等待正真写入成功。Store Buffer自动向其他核广播RFO获得独占权,等收到 ACK 后再自动写入cache中。性能提高了但是会引入延迟问题

Invalidate Queue机制则是当一个核收到上述RFO请求时,可能正在执行其他事情,不能打断,如果一直等会耽误RFO请求,所以就先将相关信息保存到Invalidate Queue后直接回复 ACK,之后有空再将做cache状态变更,性能提高了但是引入延迟问题,当Invalidate Queue未处理时可能读到一个旧数据。

如上诉问题,如果编译乱序和指令执行乱序都不存在,依然可能存在不一致问题,比如线程1执行了function1写了data,线程2执行function2依然没读到最新的值,同样可能会出现printf将打印一个非预期的data问题。

内存屏障

上述问题通常可以通过引入内存屏障解决,如下有了内存屏障可以保证变量对多个线程可见。

void function1() {
    status = true;
    smp_mb(); //内存屏障
    strcpy(data, "World");
}

内存屏障通常可以细分为写屏障、读屏障和全屏障,对于MESI协议的一致性问题,写屏障,保证 store buffer的数据写到cache中,读屏障,保证invalidate queue的请求都已经被处理。通常不同语言的volatile关键字也有内存屏障的效果。

标签:Cache,简介,cache,屏障,内存,data,CPU
From: https://blog.csdn.net/dengdui/article/details/142072005

相关文章

  • 1.Kubernetes简介
    ......
  • C# WebSocket Fleck 内存泄漏
    最近在维护公司旧项目,内存泄漏严重,找了行业内大佬帮忙分析Dump文件(windbg我不擅长),大佬指出问题在于Fleck,这里记录一下。整理一下问题:1.大佬指出System.Threading.Tasks.ContinuationTaskFromTask和System.ObjectDisposedException有71完个对象。2.System.ObjectDisposedE......
  • C语言的数据在内存中的存储
    在之前的二进制及其相关操作符与结构体内存对齐两篇文章中,我们已经对二进制数,原码反码补码进行了浅层的了解,并且也知道了高低地址以及高低字节的区别,那么既然知道了这些基础知识,就让我们借助这一层台阶,继续往更高的地方(数据在内存中的存储)大迈步吧~一、二进制数日常生活中......
  • C语言入门教程-(1)简介及搭建环境
    转载知乎https://zhuanlan.zhihu.com/p/52111695https://zhuanlan.zhihu.com/p/52259238https://zhuanlan.zhihu.com/p/52800353https://zhuanlan.zhihu.com/p/53568364https://zhuanlan.zhihu.com/p/54100371https://zhuanlan.zhihu.com/p/54278100 1.谁适合阅读本教程......
  • 【CTF】MISC常用工具集锦/使用方法简介
    前言#MISC题型多变而且工具繁杂,因此自己花时间整理了一份工具列表,以便日后参考用流畅地阅读这篇博客,你可能需要:Python2.7.18+Python3.8+任何一个更高版本的Python,使用conda管理Linux虚拟机,kali即可流畅访问Google/GitHub等站点的网络通用工具#PuzzleSolver#专为misc手......
  • 项目-高并发内存池
    本篇文章,和大家分享和一些和项目相关的知识。本次的内容主要是模拟实现一个高并发内存池。项目介绍我们这个项目的原型是google的tcmalloc,tcmalloc的全称是Thread-CachingMalloc。我们之前使用的malloc,free,本身就是一个内存池,只不过google的这个在多线程方面更高效。那我们是不......
  • C语言:数据在内存中的存储
    一.整数在内存中的存储首先,在讲解操作符的时候,我们就已经知道了,对于整形来说:数据存放内存中其实存放的是补码。并且我们也知道补码是整数的2进制表示方法之一。整数的2进制表示方法有三种,即原码、反码和补码有符号的整数,三种表示方法均有符号位和数值位两部分,符号位都是用......
  • asp.netcore8 + vue3 + mysql 自用记账项目(一)背景简介
    一、背景18年的时候,用了一年多第三方免费的记账本不用了,两个方面原因,一是随着数据增多,APP用着越来越慢,二是相关数据被用于其他用途的风险很大且广告很烦。所以,后面通过MUI+asp.netcore+sqlserver实现记账web功能,在阿里云1核2G服务器的windows系统上发布了自用的服务,最......
  • 关于GeoTools技术架构知识简介
    目录前言一、GeoTools架构说明1、GeoToolsLibrary2、各模块说明3、GeoTools插件4、GeoTools的扩展功能5、GeoTools的xml支持二、从Geotools的源码看架构1、GeoTools源码2、各功能模块介绍  3、以library来看相关组件三、总结前言        作为使用Java语言开发的地理信息......
  • 如何在 Linux 系统中查看 CPU 核数和内存大小
     在日常运维和开发中,了解服务器或虚拟机的硬件配置是非常重要的一环。无论是进行性能调优,还是资源分配,了解CPU的核数和内存大小可以帮助我们更好地规划应用的运行环境。本篇博客将介绍如何在Linux系统中查看CPU核数和内存大小。一、查看CPU核数在Linux中,查看CPU......