JAVA面试题分享五百二十六:缓存是如何工作的?

本文详细解释了Cache的工作原理,包括地址映射策略、替换策略,以及Cache的读写操作。重点讨论了缓存一致性问题、总线嗅探和MESI协议在解决多核CPU缓存一致性中的作用。同时,探讨了Tag在Cache中的作用及其硬件开销。
摘要由CSDN通过智能技术生成

目录

一、Cache的工作原理

1.1  Cache地址映射策略

1.2 Cache替换策略

二、Cache的读写操作

2.1 Cache 的读操作

2.2 Cache 的写操作

2.3 Cache 的读操作举例

三、缓存一致性问题

四、总线嗅探

五、MESI 协议

六、 多级Cache之间的配合工作

七、Cache中Tag的作用和代价

7.1 命中与缺失

7.1 Tag的硬件开销


一、Cache的工作原理

图片

                                                    图1 Cache的工作流程

Cache是CPU和主存之间的一种缓冲存储单元。CPU访问主存的数据或指令是通过访问Cache达到的,Cache和主存之间的通信是以块为单位进行整体搬移的。Cache的大小相比主存要小很多,当CPU要访问的数据正好在Cache中,称为命中,否则称为不命中。Cache和主存通过Cache地址映射策略进行关联。当CPU访问cache未命中且Cache已满,此时需要将CPU将要访问的数据整块调入cache中,cache中已存的数据需要被驱逐,该模块称为Cache替换策略。

1.1  Cache地址映射策略

根据访存的局部性原理,Cache 与主存之间的数据交互是以块(行)为单位的。如下图所示,主存有M个块,Cache有C个块,C必定远小于M。

图片

                                         图2 Cache 和主存按块存储

主存和cache间有一个映射关系。这里的映射则是通过块的地址形成映射关系。对于地址映射,首先将主存地址分成两块,高m位为主存的块地址,低b位为块内地址。

主存和缓存中对应块的大小是相同的,块内地址也是相同。在主存和Cache之间进行通信的时候是对整个块进行操作。即将主存中的某个块直接送到Cache中。字节顺序不会发生任何变化。

Cache中的标记(Tag)标记了主存块和Cache块之间的对应关系。如果将一个主存块调用到Cache当中,则将主存中的块号写到标记中,当CPU要调用主存中的某个块时,只需看该块是否已在Cache中标记,若已有标记则直接访问Cache,否则将主存中的块调用到Cache中,并进行标记。

常见的缓存的地址映射有三种方式:直接映射,组相联映射和全相联映射。

图片

1.2 Cache替换策略

缓存替换的策略有很多种,下图给出了两大类。

图片

二、Cache的读写操作

2.1 Cache 的读操作

图片

结合图1的Cache架构图,当CPU发出主存地址后,主存Cache地址映射变换模块根据块号判断是否在Cache中,若命中,则直接访问Cache中的块号和块内地址,并通过数据总线将Cache中的指令或数据传送给CPU。若未命中,则需先判断,Cache是否已满,即是否可以直接将主存中对应的块装入Cache,若是,则访问主存,通过数据总线先将指令和数据传送给CPU,再通过直接通路,将指令和数据传送给Cache缓存。如果Cache已满,则需要通过Cache替换模块来访问主存并替换Cache中的某一个块。

2.2 Cache 的写操作

写操作主要是指CPU将数据写入主存的过程,在对Cache块进入写入信息时,必须保证与被映射的主存块内的信息完全一致。

为保证Cache与主存内容一致性问题,主要采用写直达和写回。

写直达(Write Through)

在进行写操作时数据既写入Cache也写入主存。这种方法的优点就是保证主存和Cache的数据的始终一致。缺点是容易反复对主存中的某一个块进行写操作,增加了访存次数。

图片

                                                      写回(Write Back)

写回法允许CPU和Cache中的数据的不一致,在进行写操作时只把数据写入Cache而不写入主存,只有当Cache数据被替换出去时才写回主存(具体见下面实例)。

上图给出了写回的具体流程。

2.3 Cache 的读操作举例

假设有如下Cache:

  • Cache Size 128 Byte

  • Cache Line Size 8 Byte(8行,每行16Byte)

  • Way=1 直接映射缓存(对应的cache替换策略简单直接),(如Way>1,需要合适的替换策略)

  • 策略:写分配和写回机制(具体见2.2)

  • Tag Array中Tag旁边Valid位:1代表有效,0代表无效。

    注:Tag Array 存储在硬件 Cache 里,占用真实 Cache 内存。但是我们提到 Cache Size 的时候,并没有考虑 Tag 的占用。所以计算时,请忽略 Tag 占用。

  • Data Array中data Cache Line旁边的Dirty位:1代表dirty(Cache中更新过数据,与主存不一致),0代表没有写过数据,即非dirty(与主存一致)

行为1:当CPU从地址0x0654读取1个字节,Cache表现如下:

图片

  1. 根据Index找到对应的Cache Line(101=5,对应图中用绿色表示选中的Cache Line)

  2. 对应的Tag部分valid bit是有效的(Tag 为 1;如果为无效,直接判定缺失)【当系统刚启动时,Cache中的数据都应该是无效的,因为还没有缓存任何数据,以及运行过程中,cache的一些cacheline可能还是空的。Cache控制器可以根据valid bit确认当前Cache Line数据是否有效。所以,比较tag确认Cache Line是否命中之前还会检查valid bit是否有效。只有在有效的情况下,比较tag才有意义。如果无效,直接判定Cache缺失。】

  3. 有效且Tag的值不相等,因此判断发生缺失.

  4. 此时需要从地址0x0650地址加载16 Byte数据到该Cache Line中

  5. 但是,发现当前Cache Line的dirty bit置位为1,表示cache中的数据更新过。因此,Cache Line里面的数据不能被简单的丢弃,由于采用写回机制,所以需要将Cache Line中的数据0xFF…FF写回他应该在的主存地址

  6. 以Cache Line中的Tag为000001111,Index为101(与待取index相同),offset为0(因为需要Cache Line大小对齐,整行写回),所以地址为0000,0111,1101,0000,即为0x07D0

图片

7. 当写回操作完成,将主存中0x0650地址开始的16个字节0x00…00加载到该Cache Line中,并清除dirty bit。然后根据offset找到0x0654返回给CPU。

图片

三、缓存一致性问题

现在 CPU 都是多核的,由于 L1/L2 Cache 是多个核心各自独有的,那么会带来多核心的缓存一致性(Cache Coherence 的问题,如果不能保证缓存一致性的问题,就可能造成结果错误。

那缓存一致性的问题具体是怎么发生的呢?我们以一个含有两个核心的 CPU  作为例子看一看。

假设 A 号核心和 B 号核心同时运行两个线程,都操作共同的变量 i(初始值为 0 )。

图片

这时,如果 A 号核心执行了 i++ 语句的时候,为了考虑性能,使用了我们前面所说的写回策略,先把值为 1 的执行结果写入到 L1/L2 Cache 中,然后把 L1/L2 Cache 中对应的 Block 标记为脏的,这个时候数据其实没有被同步到内存中的,因为写回策略,只有在 A 号核心中的这个 Cache Block 要被替换的时候,数据才会写入到内存里。

如果这时旁边的 B 号核心尝试从内存读取 i 变量的值,则读到的将会是错误的值,因为刚才 A 号核心更新 i 值还没写入到内存中,内存中的值还依然是 0。这就是所谓的缓存一致性问题,A 号核心和 B 号核心的缓存,在这个时候是不一致,从而会导致执行结果的错误。

图片

那么,要解决这一问题,就需要一种机制,来同步两个不同核心里面的缓存数据。要实现这个机制的话,要保证做到下面这 2 点:

  • 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Write Propagation

  • 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串形化(Transaction Serialization

第一点写传播很容易就理解,当某个核心在 Cache 更新了数据,就需要同步到其他核心的 Cache 里。

而对于第二点事务的串形化,我们举个例子来理解它。

假设我们有一个含有 4 个核心的 CPU,这 4 个核心都操作共同的变量 i(初始值为 0 )。A 号核心先把 i 值变为 100,而此时同一时间,B 号核心先把 i 值变为 200,这里两个修改,都会「传播」到 C 和 D 号核心。

图片

那么问题就来了,C 号核心先收到了 A 号核心更新数据的事件,再收到 B 号核心更新数据的事件,因此 C 号核心看到的变量 i 是先变成 100,后变成 200。

而如果 D 号核心收到的事件是反过来的,则 D 号核心看到的是变量 i 先变成 200,再变成 100,虽然是做到了写传播,但是各个 Cache 里面的数据还是不一致的。

所以,我们要保证 C 号核心和 D 号核心都能看到相同顺序的数据变化,比如变量 i 都是先变成 100,再变成 200,这样的过程就是事务的串形化。

要实现事务串形化,要做到 2 点:

  • CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心;

  • 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。

那接下来我们看看,写传播和事务串形化具体是用什么技术实现的。

四、总线嗅探

写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常见实现的方式是总线嗅探(Bus Snooping

以前面的 i 变量例子来说明总线嗅探的工作机制,当 A 号 CPU 核心修改了 L1 Cache 中 i 变量的值,通过总线把这个事件广播通知给其他所有的核心,然后每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面,如果 B 号 CPU 核心的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache。

可以发现,总线嗅探方法很简单, CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这无疑会加重总线的负载。

另外,总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串形化

于是,有一个协议基于总线嗅探机制实现了事务串形化,也用状态机机制降低了总线带宽压力,这个协议就是 MESI 协议,这个协议就做到了 CPU 缓存一致性。

五、MESI 协议

MESI 协议其实是 4 个状态单词的开头字母缩写,分别是:

  • Modified,已修改

  • Exclusive,独占

  • Shared,共享

  • Invalidated,已失效

这四个状态来标记 Cache Line 四个不同的状态。

「已修改」状态就是前面提到的脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存里。而「已失效」状态,表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。

「独占」和「共享」状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的。

「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

另外,在「独占」状态下的数据,如果有其他核心从内存读取了相同的数据到各自的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态

那么,「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。

我们举个具体的例子来看看这四个状态的转换:

  1. 当 A 号 CPU 核心从内存读取变量 i 的值,数据被缓存在 A 号 CPU 核心自己的 Cache 里面,此时其他 CPU 核心的 Cache 没有缓存该数据,于是标记 Cache Line 状态为「独占」,此时其 Cache 中的数据与内存是一致的;

  2. 然后 B 号 CPU 核心也从内存读取了变量 i 的值,此时会发送消息给其他 CPU 核心,由于 A 号 CPU 核心已经缓存了该数据,所以会把数据返回给 B 号 CPU 核心。在这个时候, A 和 B 核心缓存了相同的数据,Cache Line 的状态就会变成「共享」,并且其 Cache 中的数据与内存也是一致的;

  3. 当 A 号 CPU 核心要修改 Cache 中 i 变量的值,发现数据对应的 Cache Line 的状态是共享状态,则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 中的数据就与内存不一致了。

  4. 如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值,由于此时的 Cache Line 是「已修改」状态,因此不需要给其他 CPU 核心发送消息,直接更新数据即可。

  5. 如果 A 号 CPU 核心的 Cache 里的 i 变量对应的  Cache Line 要被「替换」,发现  Cache Line 状态是「已修改」状态,就会在替换前先把数据同步到内存。

所以,可以发现当 Cache Line 状态是「已修改」或者「独占」状态时,修改更新其数据不需要发送广播给其他 CPU 核心,这在一定程度上减少了总线带宽压力。

事实上,整个 MESI 的状态可以用一个有限状态机来表示它的状态流转。还有一点,对于不同状态触发的事件操作,可能是来自本地 CPU 核心发出的广播事件,也可以是来自其他 CPU 核心通过总线发出的广播事件。

下图即是 MESI 协议的状态图:

图片

六、 多级Cache之间的配合工作

当CPU试图从某地址load数据时,下图为只有两级Cache的系统举例:

  • 从L1 Cache中查询是否命中,如果命中则把数据返回给CPU(蓝色实线)

  • L1 Cache缺失,则继续从L2 Cache中查找。当L2 Cache命中时,数据会返回给L1 Cache以及CPU(绿色实线&绿色虚线)

  • L2 Cache也缺失,需要从主存中load数据,将数据返回给L2 Cache、L1 Cache及CPU(红色实线&红色虚线)

图片

这种多级Cache的工作方式称之为inclusive Cache。某一地址的数据可能存在多级缓存中。与Inclusive Cache对应的是Exclusive Cache,这种Cache保证某一地址的数据缓存只会存在于多级Cache其中一级。也就是说,任意地址的数据不可能同时在L1和L2 Cache中缓存。

七、Cache中Tag的作用和代价

以一个Cache Size 为 128 Bytes 并且Cache Line是 16 Bytes的Cache为例。

图片

实际上,Cache主要由两部分组成,Tag部分和Data部分。因为cache利用了程序中的相关性,一个被访问的数据,它本身和它周围的数据在最近都有可能被访问,因此Data部分就是一个数据块,而Tag部分则表示该数据块对应主存的区号, 一个Tag和它对应的所有数据Data组成一行称为cache line,而cache line中的数据部分称为数据块(cache data block)。

图片

7.1 命中与缺失

  • 假设地址总线是16位,目标地址为0x0654,转换为二进制为 0000,0110,0101,0100

  • Offset:由于每个Cache Line中有16 Byte,所以地址最低4位,即为每一个Cache Line中的偏移Offset,标记在这个Cache Line中的具体位置是哪个字节,举例中为0100,即为图中地址段的蓝色背景部分。

  • Index:由于一共有8个Cache Line,所以地址除去最低4位的后3位,即为不同Cache Line的索引Index,标记具体在整个Cache 中的那一个Cache Line,举例中为101,即为图中地址段的绿色背景部分。

图片

如果两个不同的地址,其地址的Index部分完全一样,这两个地址经过硬件散列之后都会找到同一个Cache Line。所以,根据地址确定到Cache Line之后,只代表所需要访问的目标地址中存储的对应数据可能存在这个Cache Line中,但是该Cache Line也有可能存储其他地址对应的数据。

所以,独立于Data Array,又引入Tag Array区域,Tag Array和Data Array中的每一个Cache Line都有着一一对应关系。每一个Cache Line都对应唯一一个tag,tag中保存的是整个地址。位宽去除index和offset使用的bit剩余部分(如上图地址粉色背景部分)。tag、index和offset三者组合就可以唯一确定一个地址。

因此,根据地址中index位找到Cache Line后,取出当前Cache Line对应的tag,然后和目标地址的tag进行比较,如果相等,这说明Cache命中。如果不相等,说明当前Cache Line存储的是其他地址的数据,这就是Cache缺失。

在上述图中,我们看到tag的值是0,0000,1100,和地址中的tag部分相等,因此在本次访问会命中。

7.1 Tag的硬件开销

由于Tag Array 也是Cache的一部分,存储在硬件 Cache 里,占用真实 Cache 内存。因此tag的引入会导致硬件成本的上升,将两种情况进行对比:

  • 原本Cache Line 设置为16 Byte:每16 Byte对应一个tag,需要8个tag

  • 假设Cache Line设置为1 Byte:需要128个Tag同时每一个Tag的长度也会更长,因为Offest缩短了。

因此可以发现后者占用了很多内存。

  • 16
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值