1.从一个示例代码说起

探讨内存屏障的问题基本都会从如下代码作为示例讲解:

//假设a和b初始化为0 ,CPU 0执行foo函数,CPU 1执行bar函数。我们再进一步假设a变量
//在CPU 1的cache中,b在CPU 0 cache中,执行的操作序列如下:
CPU0:
void foo(void) {a = 1;b = 1;
}CPU1:void bar(void) {while (b != 1); assert (a == 1);
}

两方便原因可能导致assert失败:

  • 编译层面

    • 如果前后代码无数据依赖关系,如果编译优化选项(-O2或者-O3)级别比较高会发生代码乱序,提升性能。
    • 注意:c/c++的关键字volatile只保证内存即使从寄存器回写到主存,没有内存屏障功能,也不保证原子性。
  • CPU层面
    • 现在的CPU处理器支持乱序执行(out-of-order)。如果刚刚接触这个概念,十有八九会对乱序执行产生误解,以为是因为单纯的cpu指令乱序执行导致了上面示例代码的assert失败,其实不然。

乱序执行的本质: 

乱序执行是说,给定一串执行指令,cpu为了提升执行效率,会找出没有数据依赖的指令,让他们并行执行,但是,执行执行结果写回寄存器是顺序的。哪怕是先被执行的指令,它的运算结果也是按照指令次序写回到最终的寄存器的。这个和很多程序员理解的乱序执行是有区别的。所以在单核处理器系统中,虽然CPU内部支持乱序执行,但是CPU会保证最终执行结果符合程序员的要求,这种情况下不需要使用内存屏障。这里从linux内核内存屏障函数定义也可以看出:

#if defined(CONFIG_ARM_DMA_MEM_BUFFERABLE) || defined(CONFIG_SMP)
#define mb()        __arm_heavy_mb()
#define rmb()       dsb()
#define wmb()       __arm_heavy_mb(st)
#define dma_rmb()   dmb(osh)
#define dma_wmb()   dmb(oshst)
#else
#define mb()        barrier()
#define rmb()       barrier()
#define wmb()       barrier()
#define dma_rmb()   barrier()
#define dma_wmb()   barrier()
#endif

可以看到如果是非SMP系统,内存屏障都定义成barrier()函数,这个函数就是编译器层面的内存屏障函数,避免编译器乱序执行代码。

2.为什么SMP系统会乱序

乱序的核心原因是现代cpu为了追求性能,实现的memory model有关,现代cpu内部有store buffer/cache/invaliate queue和缓存一致性协议的方案导致乱序的可能。尤其arm64处理起是强乱序的处理起,读写,写读,读读,写写都可能乱序。

2.1 现代cpu架构存储图:

store buffer:

举例说明引入store buffer原因:

  • 变量不在cpu的cache中,cpu想修改这个变量,需要发送read invalidate信号,等待信号返回之后才可以写入缓存cache中,如果cpu核数很大,等到每个cpu响应该信号导致性能差。
  • 变量量在该CPU缓存中,如果该量的状态是exclusive则直接更改。而如果是shared则需要发 送invalidate消息让其它CPU感知到这一更改后再更改。

有了store buffer之后,cpu可以将变量写入store buffer,然后去忙其他事情,其他cpu响应信号之后再将store buffer中的变量写入缓存。

2.2 store buffer引入的乱序

store buffer提升了性能(本处解释假设不存在invalidate queue,方便理解问题),却引入了复杂性,考虑如下顺序:

  1. cpu0执行a = 1指令,其cache中值是0,那么由于store buffer的引入,cpu0直接将a = 1写入了store buffer,不需要等待cpu1的信号响应就可以执行b = 1。
  2. cpu0执行b = 1时,由于b在cpu0中的状态是exclusive独占(可以去参考文章看MESI协议),cpu0直接将b = 1写入了cpu 0 cache中,那么cpu1执行b != 1 成功,然后执行assert( a == 1),由于cpu0需要等到cpu1响应完invalidate才将 a= 1写入cache中(目前还在store buffer中),所以cpu1读取到的任然是0,assert失败。

从图例可以看到第六步assert失败的核心原因在于,cpu0上缓存过b,所以cpu0执行b = 1k立马写入cache,然后线程B所在的cpu1执行while(b==0)迅速跳出,然后执行 a = 1,由于此时 a 还在cpu0的store buffer中,所以导致assert失败。

2.3 解决方案

void foo(void) {a = 1;smp_wmb();b = 1;
}

smp_mb保证 b = 1执行前,先把store buffer内存刷新到cache中。CPU1 执行assert( a == 1),发现a 不在cache中,向CPU0发送read消息,CPU0的cache中存在a,所以CPU1获取到a = 1,assert成功。

2.4 invalidate queue引入的乱序

引入原因:

由于store buffer的大小是有限,上面例子中如果cpu1无法快速处理read invalidate消息,那么cpu0中的store buffer马上就会满了,所以arm增加了invalidate queue存放收到invalidate消息,收到invalidate消息放入invalidate queue中,然后给对方(cpu0)发送一个respone响应,但是此时并不真正处理invalidate消息,正因如此,invalidate queue又引入新的复杂性。

invalidate queue导致的乱序:

还是使用上面的代码,始状态CPU0拥有b=0(独有),存有a=0(shared),CPU1存有a=0(shared),CPU0执行foo,CPU1执行bar。

1和2.  CPU0 执行 a = 1,由于a是shared的状态,不能直接写入cache,先放入store buffer,发送invalidate消息(等到CPU1响应Invalidate消息才从store buffer写入cache)

3. CPU1执行while 循环,由于cache中不存在b,发送read消息给CPU0。

4.CPU0执行b = 1,由于b是cache独有,直接写入cache

5. CPU1收到Invalidate消息,直接放入invalidate queue

6. CPU1收到read消息回复,同步到b = 1,结束while循环

7. CPU1执行assert,由于此时CPU1未处理invalidate消息,cache中 a = 0,所以assert失败。

这种情况失败的主要原因是,CPU1不及时的处理invalidate queue的消息,导致cache中的数据是失效的。

2.5 解决方案

void bar(void) {while (b != 1); smb_rmb();assert (a == 1);
}

3. 内存屏障指令

参考:ARM64中的内存屏障指令 - 知乎

参考文章:

Why Memory Barriers?中文翻译(上)3

ARM64中的内存屏障指令 - 知乎

https://people.freebsd.org/~lstewart/articles/cpumemory.pdf

CPU乱序执行_楓潇潇的博客-CSDN博客_cpu乱序执行

彻底搞懂内存屏障(上)相关推荐

  1. 【嵌入式】初学者一步一步搞懂内存管理

    [嵌入式]初学者一步一步搞懂内存管理 一.C语言局部变量.静态局部变量.全局变量与静态全局变量 基本概念 局部变量 全局变量 局部变量和全局变量的对比 二.虚拟地址空间.(深入理解计算机系统) bss ...

  2. 可能你的EventBus使用并不正确,是时候真正搞懂EventBus了(上)

    EventBus 1.EventBus使用 2.常规原理分析 2-1.`EventBus.getDefault().register(Object subscriber)` 2-1-1.`subscr ...

  3. 一文搞懂内存映射原理及使用方法

    a. 内存映射原理 内存映射即在进程的虚拟地址空间中创建一个映射,分为两种: 文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间,数据源是存储设备上的文件. 匿名映射:没有文件支持 ...

  4. 程序在计算机中是如何运行的?搞懂内存和CPU(*)

    1.程序在计算机中是如何运行的? 运算器 控制器 存储器 输入设备 输出设备 在计算机中,保存信息主要靠存储器,而存储器又分为内部存储器和外部存储器,内部存储器就是内存,外部存储器主要就是磁盘,磁盘又 ...

  5. C语言实现单例模式,以及使用内存屏障的性能优化方案

    这里有一篇关于<C语言实现简单的单例模式>的基于OpenMP多线程的单例模式示例程序,这里给出采用内存屏障由于单例模式的示例"https://coderatwork.cn/pos ...

  6. 内存屏障什么的(经典)

    转载:http://www.spongeliu.com/clanguage/memorybarrier/ 当你看到"内存屏障"四个字的时候,你的第一反应是什么?寄存器里取出了错误的 ...

  7. java volatile内存屏障_从汇编看Volatile的内存屏障

    Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的,让我们看看从字节码以及汇编码的角度,来看下是否真是如此? 一 Volatile与内存屏障 为了实现volat ...

  8. 【JVM】一文搞懂常见GC算法

    文章内容 1.概述 2.如何确定垃圾对象? 3.GC算法 4.GC算法总结 5.常见的垃圾收集器 1.概述 GC目的:程序运行过程中可能会产生许多垃圾对象,持续占用内存会造成内存泄漏,最终可能导致内存 ...

  9. 内存屏障与java的内存屏障 —— JVM篇

    内存屏障与java的内存屏障 内存屏障 前言 一.什么是内存屏障? 二.volatile变量规则 1.volatile简介 2.volatile原理 3.volatile特性 4.volatile变量 ...

最新文章

  1. FPGA设计细节和实现(初学者)
  2. python正则表达式代码_python的re正则表达式实例代码
  3. Cache超清晰逻辑详解(cache的三种映射)
  4. C# 与 VC Dll 传输信息
  5. Weblogic EJB 学习笔记(3)精
  6. python找出一个数的所有因子_python – 找到最大素因子的正确算法
  7. 远程服务器返回错误: (405) 不允许的方法_四指炸鸡总部远程协助选址,5大加盟优势,0基础即可开店...
  8. Ubuntu下安装tilix终端仿真器
  9. 一键搞定数码照片印前特效-【用可牛影像】
  10. 凸优化第四章凸优化问题 4.7向量优化
  11. 一文学懂经典算法系列之:直接选择排序(附讲解视频)
  12. ZebraDesigner-设计label
  13. 电路中的输入输出阻抗以及阻抗匹配
  14. 常见的Hash算法(General Purpose Hash Function Algorithms)
  15. 副本全攻略之哀号洞穴(超详细)
  16. Android USB Tethering的实现以及代码流程
  17. 《梵高》-孤独的天才
  18. C语言学习日记(3)——printf函数
  19. DeFi热潮下的安全隐患:流动性危机恐将造成连锁反应 | 非正式会谈
  20. =,==,===的区别

热门文章

  1. 组合数有关的公式及常用求和【数学--排列组合】
  2. 脑科学研究中基于图论的复杂脑网络分析方法
  3. 让AI做作业:基于PaddleNLP-Taskflow的错别字单项测试
  4. Microsoft Visusl C++2010运行程序时,调试弹出黑框自动闪退无法看见运行结果的解决方法
  5. VirtualProtect 3方法 -seh ret-ASLR-dep-Adrenalin Player 2.2.5.3
  6. kirin710f是什么处理器_kirin710什么处理器
  7. 硬盘IOPS与读写速度
  8. 车载滤波器组件焊锡开裂失效分析
  9. Excel收纳箱:如何通过VBA获得包含数据的最大行
  10. 河北万豪环保紫外线消毒器普及知识