您的位置:首页 > 其它

Cache一致性协议与伪共享问题

2022-01-10 23:11 253 查看 https://www.cnblogs.com/franki

Cache一致性协议

在说伪共享问题之前,有必要聊一聊什么是Cache一致性协议

局部性原理

时间局部性:如果一个信息项正在被访问,那么在近期它很可能还会被再次访问
比如循环、方法的反复调用等

空间局部性:如果一个存储器的位置被引用,那么将来他附近的位置也会被引用
比如顺序结构、数组

Cache的作用

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存Cache,以解决访存速度和CPU运算速度之间不匹配的问题

带Cache的CPU访存过程

CPU和Cache交换数据以为单位。Cache与主存以为单位,一个缓存行(Cache Line)对应一个主存块

  • Cache命中,则直接从Cache中读取数据
  • Cache不命中,则访问主存,并将一个主存块调入Cache中,存入为一个缓存行。这个过程中可能由于Cache满而发生替换,替换算法包括RAND、FIFO、LRU、LFU

  • Cache命中时 写回法(write back):CPU只将数据写入Cache,只有当数据调出Cache时,才写入主存
  • 写穿法(write through):CPU同时将数据写入Cache和主存
  • Cache不命中时
      写分配法:从主存中将数据块调入Cache,并修改Cache,和写回法配合使用
    • 非写分配法:只写入主存,不调入Cache,和写穿法配合使用

    Cache和主存的映射方式(三种):直接映射、全相联映射、组相联映射

    如果是单CPU结构,这么执行没有其他问题。但是现代系统往往包含多个CPU,每个CPU都有各自的Cache。多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致性,不让系统数据混乱。这里就引出了Cache一致性协议——MESI。注意,这并不是唯一的缓存一致性协议,还有其他协议如MOSEI(相对于MESI多引入了一个Owned状态,并重新定义了S状态),这里不多介绍

    MESI协议详解

    MESI(Modified Exclusive Shared Or Invalid),也称伊利诺斯协议,是一种广泛使用的、支持写回策略的缓存一致性协议。MESI协议其实就是使用4种状态来标记各个缓存行(Cache Line)的状态,而这些状态英文首字母缩写就构成了“MESI”

    MESI协议中的各种状态

    每个缓存行都使用一个状态来标记,该状态总共有4种,使用2bit进行存储:

    • M(Modified):相应的数据只被缓存在该CPU的Cache中,但数据是被修改过的(脏数据),即与主存中的数据不一致。该缓存行中的内存需要在未来的某个时间点,但必须是其它CPU读取主存中相应内存之前,将数据写回主存
    • E(Exclusive):相应的数据只被缓存在该CPU的缓存中,数据是未被修改过的,与主存中的数据一致
    • S(Shared):相应的数据被多个CPU缓存,且各个CPU的Cache中的数据和主存都是一致的
    • I(Invalid):该缓存行中的数据是无效的,因为有其他CPU修改了数据

    总线嗅探机制(监听)

    每个CPU都可以感知其他CPU的行为,比如读、写某个缓存行,这就是嗅探机制,也称监听。所有的缓存行(除了Invalid状态)都需要监听自己和其他CPU对相应的缓存行的读写操作,也称触发事件,从而根据触发事件和自身状态,进行状态的转换

    各种触发事件

    触发事件 描述
    本地读取(Local Read) 本CPU读取本Cache的数据
    本地写入(Local Write) 本CPU向本Cache写入数据
    远端读取(Remote Read) 其他CPU读取它们各自Cache的数据
    远端写入(Remote Write) 其他CPU向它们各自Cache写入数据

    MESI中各个状态之间的转换

    下图描述了当前缓存行在不同触发事件下的状态切换:

    下表是对上图的一个详细解释:

    举例

    假设CPU0、CPU1、CPU2、CPU3中有一个缓存行(包含变量x)都是S状态

    此时CPU1要对变量x进行写操作,这时候通过总线嗅探机制,CPU0、CPU2、CPU3中的缓存行会置为I状态(无效),然后给CPU1发响应,收到全部响应后CPU1会完成对变量x的写操作,并更新CPU1内的缓存行为M状态,但不会将数据x同步到主存中

    接着CPU0想要对变量x执行读操作,却发现本地缓存行是I状态,就会触发CPU1去把缓存行写回到主存中,然后CPU0再去主存中同步最新的值

    其他一些细节

    1、写缓冲
    前面的描述隐藏了一些细节,比如实际CPU1在执行写操作,更新缓存行的时候,其实并不会等待其他CPU的状态都置为I状态,才去做些操作,这是一个同步行为,效率很低。当前的CPU都引入了写缓存器技术,也就是在CPU和cache之间又加了一层buffer,在CPU执行写操作时直接向写缓冲写入数据,然后就忙其他事去了,等其他CPU都置为I之后,CPU1才把buffer中的数据写入到缓存行中

    2、多级缓存

    现代系统都会采用多级缓存架构,L1-L3级缓存,其中L3缓存是所有CPU共享的一个缓存,但MESI的描述中并没有涉及L3缓存。其实上文提到的所有跟“主存”交换数据的地方,在L3缓存存在的情况下,都应该替换为L3缓存。比如我上一节举的例子中,CPU0中某缓存行是I,CPU1 中是M。当CPU0想到执行local read操作时,就会触发CPU1中的缓存写入到主存中,然后CPU0从主存中取最新的缓存行。其实这里的描述是不准确的,因为由于L3缓存的存在,这里其实是直接从L3缓存读取缓存行,而不直接访问主存。个人认为是如果在描述MESI的状态流转时,如果引入L3缓存,会使得描述过于复杂,因此一般的描述都会刻意忽略L3缓存

    作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15786362.html
    版权:本文版权归作者和博客园共有
    转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

    伪共享

    由于内存和Cache之间的交换单位是内存块/缓存行,因此如果访问一个变量,会将一整个内存块读入一个缓存行。但是如果多个线程访问的变量不相同,且这些变量在内存中的位置临近,那么很可能在同一个缓存行中。在Cache一致性协议(如MESI协议)的约束下,多个线程(CPU)在并行读写相对应的缓存行会有限制,因此其他线程不得不去访问低级别的Cache甚至是主存,这会导致cache没有起到真正的作用,程序性能下降

    伪共享示例

    如图,线程1访问变量x,而线程2访问变量y,而这两个变量在内存中的位置临近(在同一个内存块中),虽然线程都将该内存块读入到各自的工作内存(Cache)中,但是在Cache一致性协议的约束下,同一时间两个线程很难自由地读写相同位置的缓存行,那么可能就会让其中一个线程去低级别的内存中读写数据,性能因此降低,这就是伪共享

    一般地址连续的多个变量更可能被放在同一个缓存行中,例如创建数组时,数组中的多个元素更可能被放入同一个缓存行中

    如何避免伪共享问题

    JDK8之前
    JDK8之前,使用字节填充的方式,即创建一个变量时,使用填充字段填充该变量所在的缓存行,从而避免多个变量被放入同一个缓存行中,如下:

    public final static class FilledLong {
    public volatile long value = 0L;
    public long p1, p2, p3, p4, p5, p6;
    }

    一般来说,缓存行为64 Byte,而经过填充的

    FilledLong
    对象有7*8 Byte=56 Byte,而
    FilledLong
    对象的对象头也有8 Byte,正好填满一个缓存行

    JDK8及之后
    JDK8提供了一个注解——

    sun.misc.Contended
    ,用于解决伪共享问题,上述代码可以修改为如下:

    @sun.misc.Contended
    public final static class FilledLong {
    public volatile long value = 0L;
    }

    Thread
    类中,也有这样的字段,如下:

    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;
    
    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;
    
    /** Secondary seed isolated from public ThreadLocalRandom sequence */
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;

    但是,

    @Contended
    注解只用于Java核心类,而用户类路径下的类使用该注解,需要添加JVM参数
    -XX:-RestrictContended
    。填充宽度默认为128 Byte,也可以自定义宽度,通过JVM参数
    -XX:ContendedPaddingWidth
    来设定

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: