首页 > 系统相关 >volatile及内存屏障理解总结

volatile及内存屏障理解总结

时间:2024-02-24 18:22:35浏览次数:33  
标签:store buffer cache 屏障 volatile 内存 CPU 乱序

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些未知的因素更改。volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。所以遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
C/C++中的volatile不提供任何防止乱序的功能,也并不保证访存的原子性。
多线程情况下,在本次线程内, 当读取一个变量时,编译器优化时有时会先把变量读取到一个寄存器中;以后再取变量值时,就直接从寄存器中取值;当内存变量或寄存器变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

编译时乱序

现代编译器的代码优化和编译器指令重排可能会影响到代码的执行顺序。编译期指令重排是通过调整代码中的指令顺序,在不改变代码语义的前提下,对变量访问进行优化。从而尽可能的减少对寄存器的读取和存储,并充分复用寄存器。但是编译器对数据的依赖关系判断只能在单执行流内,无法判断其他执行流对竞争数据的依赖关系。就拿无锁环形队列来说,如果Writer做的是先放置数据,再更新索引的行为。如果索引先于数据更新,Reader就有可能会因为判断索引已更新而读到脏数据

运行时内存乱序访问

CPU还有乱序执行(Out-of-Order Execution)的特性。流水线(Pipeline)和乱序执行是现代CPU基本都具有的特性。机器指令在流水线中经历取指、译码、执行、访存、写回等操作。为了CPU的执行效率,流水线都是并行处理的,在不影响语义的情况下。处理器次序(Process Ordering,机器指令在CPU实际执行时的顺序)和程序次序(Program Ordering,程序代码的逻辑执行顺序)是允许不一致的。
我们了解到每个CPU都会有自己私有L1 Cache。L1 Cache命中的情况下,访问数据一般需要2个指令周期。而且当CPU遭遇写数据cache未命中时,内存访问延迟增加很多。硬件工程师为了追求极致的性能,在CPU和L1 Cache之间又加入一级缓存,我们称之为store buffer。store buffer和L1 Cache还有点区别,store buffer只缓存CPU的写操作。store buffer访问一般只需要1个指令周期,这在一定程度上降低了内存写延迟。不管cache是否命中,CPU都是将数据写入store buffer。

arm/power架构

x86架构
x86上cpu核和cache以及内存之间,存在着store buffer,当cpu0写操作执行成功后,修改只存在于store buffer中,并未写到cache以及内存上,因此cpu1读取不到最新的x值。对于arm/power来说,同样也有store buffer,而且还可能会有invalid queue,导致cpu1读不到最新的x值。

对于没有invalid queue的x86系列cpu来说,当修改从store buffer刷入cache时,就能够保证在其他核上能够读到最新的修改。但是,对于存在invalid queue的cpu来说,则不一定。

store buffer存在于cpu核与cache之间,对于x86架构来说,store buffer是FIFO,因此不会存在乱序,写入顺序就是刷入cache的顺序。CPU0的store-load操作,在别的CPU看来乱序执行了,变成了load-store次序(等待其他core给它response的时候,就可以先写store buffer,然后继续后面的读操作,对外表现就是写读乱序。)。这种内存模型,我们称之为完全存储定序(Total Store Order),简称TSO。store和load的组合有4种。分别是store-store,store-load,load-load和load-storeTSO模型中,只存在store-load存在乱序,另外3种内存操作不存在乱序(store-store由fifo队列保证,没有)。
但是对于ARM/Power架构来说,store buffer并未保证FIFO,因此先写入store buffer的数据,是有可能比后写入store buffer的数据晚刷入cache的。从这点上来说,store buffer的存在会让ARM/Power架构出现乱序的可能。store barrier存在的意义就是将store buffer中的数据,刷入cache

在某些cpu中,存在invalid queue。invalid queue用于缓存cache line的失效消息,也就是说,当cpu0写入W0(x, 1),并从store buffer将修改刷入cache,此时cpu1读取R1(x, 0)仍是允许的。因为使cache line失效的消息被缓冲在了invalid queue中,还未被应用到cache line上。这也是一种会使得指令乱序的可能。load barrier存在的意义就是将invalid queue缓冲刷新

总结

volatile仅解决查询数据时从内存地址直接获取,不读缓存。而内存屏障解决程序中多语句可能存在乱序的情况,造成脏读或者脏写。一个解决的是这个值当前是否最新的,一个解决的是当前运行的语句顺序是否符合代码编写的预期。
如果不做多余的防护措施,单核时代的无锁环形队列在多核CPU中,一个CPU核心上的Writer写入数据,更新index后。另一个CPU核心上的Reader依靠这个index来判断数据是否写入的方式不一定可靠。index有可能先于数据被写入,从而导致Reader读到脏数据。
写屏障:防止store store 乱序,读屏障:防止load load乱序,全屏障:防止4种乱序。
在具有强内存排序(如:x86)的机器上,这些较弱(读、写)的屏障将防止编译器重新排列,而不会发出任何实际的机器代码
在内存排序较弱(如:arm)的机器上,它们将阻止编译器重新排序并且还发射可能需要的任何硬件屏障。即使在内存排序较弱的机器上,读或写屏障可能使用比全屏障更便宜的指令。

With these guidelines in mind, the writer can do this:
    // 如果不加屏障,q->items[q->num_items] 只写入到store buffer,其他cpu感知不到,q->num_items可能已经写入到cache,其他cpu可以感知,则有可能读取到脏数据
    q->items[q->num_items] = new_item;
    pg_write_barrier();
    ++q->num_items;

And the reader can do this:
    // 如果不加屏障,num_items可能读取的时失效的值,而q->items[i]可能是新的value,此时如果用num_items 处理数组,则消费不全
    num_items = q->num_items;
    pg_read_barrier();
    for (i = 0; i < num_items; ++i)
        /* do something with q->items[i] */

c++ - Why do we use the volatile keyword? - Stack Overflow
C语言丨深入理解volatile关键字 - 知乎
当我们在谈论cpu指令乱序的时候,究竟在谈论什么? - 知乎
内存一致性模型-TSO - 知乎
x86 架构下 StoreLoad 屏障-CSDN博客
聊聊原子变量、锁、内存屏障那点事 | 浅墨的部落格
multithreading - How do I Understand Read Memory Barriers and Volatile - Stack Overflow
理解 Memory barrier(内存屏障)无锁环形队列_无锁循环队列 内存屏障-CSDN博客
PgSQL · 源码分析 · PG中的无锁算法和原子操作应用一则-阿里云开发者社区

标签:store,buffer,cache,屏障,volatile,内存,CPU,乱序
From: https://www.cnblogs.com/sanguoasd/p/18030420

相关文章

  • 内存和磁盘的亲密关系
    本章,我了解到什么是磁盘,磁盘也内存的关系如何提高内存的利用效率磁盘是一种永久性存储介质,用于长期保存数据和程序。它通常由硬盘驱动器(HDD)或固态硬盘(SSD)组成。磁盘以扇区为单位进行数据存储,每个扇区的大小通常为512字节或4KB。磁盘具有较大的存储容量,但读写速度相对较慢。磁盘......
  • 熟练的使用有棱有角的内存
    在我们使用计算机的生活中,我们当然离不开内存这个东西,我们常常应为内存不够用而苦恼,因此从而挑选多的内存,我们不妨去了解内存的底层逻辑,从而更好的使用它,那么内存又是什么呢?我们该如何使用它呢?什么是内存内存是计算机中最重要的部件之一,它是程序与CPU进行沟通的桥梁。计算机中所......
  • 内存和磁盘的亲密关系
    程序保存在存储设备中,通过有序的被读出来实现运行,这一机制称为存储程序方式(程序内置方式)。计算机中主要的存储部件是内存和磁盘。磁盘中存储的程序,必须要加载到内存后才能运行。磁盘中保存的原始程序是无法直接运行的。这是因为,负责解析和运行程序内容的CPU,需要通过内部程序计数器......
  • 刘铁猛C#学习笔记3 类型、变量、对象、内存
    一、C#中的类型 二、类型所能表示的数的范围其中S开头代表带符号(用一位来存储符号),U开头代表无符号8位=1字节byte 三、程序的静态与动态:静态-尚未运行,在编译器中编译动态-正在运行、调试 程序不运行时在硬盘(外存)里,称作静态的运行时装载到内存里,称作动态的  ......
  • linux cpu 内存分析
    1.通过分析服务器资源,当发现资源消耗过多时,需要分析什么进程占用了,如下所示 2.分析第一台服务器通过登录服务器,使用top命令查看,出来信息如下所示: 进程182618的内存占用了52.6%,属于.net应用程序,通过已维护的文档,知道了哪些.net程序的部署了,最终找到是该web应用程序......
  • 多线程系列(七) -ThreadLocal 用法及内存泄露分析
    一、简介在Javaweb项目中,想必很多的同学对ThreadLocal这个类并不陌生,它最常用的应用场景就是用来做对象的跨层传递,避免多次传递,打破层次之间的约束。比如下面这个HttpServletRequest参数传递的简单例子!publicclassRequestLocal{/***线程本地变量*/......
  • 一种用于多线程中间状态同步的屏障机制
    一种用于多线程中间状态同步的屏障机制为了解决在多线程环境中,需要一个内置的计数屏障对于多个线程中的某一个部分进行检查,确保所有线程均到达该点后才能继续执行。该屏障常被用于多线程流水线中的中间检查,适用于阶段分割,是一种有效的同步机制。此处构建了一个barrier类,其中arr......
  • C++动态内存分配探秘:new与malloc的关键差异及实例解析
     概述:在C++中,new和malloc均用于动态内存分配,但存在关键差异。new是C++运算符,能调用构造函数,返回类型明确;而malloc是C函数,仅分配内存,需手动类型转换。示例源代码生动演示了它们在构造函数调用和类型信息方面的不同。在C++中,new 和 malloc 都用于动态内存分配,但它们之间......
  • 深入理解C++中的堆与栈:内存管理的关键区别与实例解析
     概述:C++中,堆和栈是两种不同的内存分配方式。栈自动分配、释放内存,适用于短生命周期变量;堆需要手动管理,适用于动态分配内存,但需要显式释放以防内存泄漏。通过清晰的示例源代码,演示了它们在变量生命周期、访问方式等方面的区别。C++中的堆(heap)和栈(stack)是两种内存分配和管理方......
  • 第五章 内存与磁盘的密切关系
    在阅读《程序是怎样跑起来的》这本书的第五章时,我被作者对于内存与磁盘之间密切关系的深入剖析所吸引。这一章不仅详细描述了内存和磁盘在计算机系统中的作用,还深入探讨了它们之间的交互和相互依赖。读完这一章后,我对计算机的内存和磁盘有了更深入的理解。首先,我深刻认识到了内存......