当前位置: 首页 > news >正文

CPU缓存一致性

部分内容来源:小林coding


CPU Cache的数据写入方式

事实上,数据不光是只有读操作,还有写操作

如果数据写入 Cache 之后,内存与 Cache 相对应的数据将会不同

这种情况下 Cache 和内存数据都不一致了,我们肯定是要把 Cache 中的数据同步到内存里的

问题来了,那在什么时机才把 Cache 中的数据写回到内存呢?

为了应对这个问题,下面介绍两种针对写入数据的方法:

  1. 写直达(Write Through)
  2. 写回(Write Back)

写回

保持内存与 Cache 一致性最简单的方式是,把数据同时写入内存和 Cache 中

这种方法称为写直达(Write Through)

在这个方法里,写入前会先判断数据是否已经在 CPU Cache 里面了:

  • 如果数据已经在 Cache 里面,先将数据更新到 Cache 里面,再写入到内存里面;
  • 如果数据没有在 Cache 里面,就直接把数据更新到内存里面(相当于 “绕开缓存,直接存硬盘”)

写直达法很直观也很简单,但是问题明显

无论数据在不在 Cache 里面,每次写操作都会写回到内存,这样写操作将会花费大量的时间,无疑性能会受到很大的影响


写直达

核心逻辑:写操作只更新缓存内存延迟到 “缓存块被替换 / 清理时” 才同步

写回的逻辑:先攒着,等替换时再同步

缓存不命中的时候修改缓存,将要修改位置的脏数据刷新到内存

写直达由于每次写操作都会把数据写回到内存,而导致影响性能

为了要减少数据写回内存的频率就出现了写回(Write Back)

在写回机制中,当发生写操作时

新的数据仅仅被写入 Cache Block 里

只有当修改过的 Cache Block「被替换」时才需要写到内存中

减少了数据写回内存的频率,这样便可以提高系统的性能

那具体如何做到的呢?下面来详细说一下:

  1. 如果当发生写操作时,数据已经在 CPU Cache 里的话,则把数据更新到 CPU Cache 里,同时标记 CPU Cache 里的这个 Cache Block 为脏(Dirty)(标记脏数据),这个脏的标记代表这个时候,我们 CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的,这种情况是不用把数据写到内存里的;
  2. 如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话就要检查Cache Block里是否有脏数据
  • 如果是脏数据:将Cache Block 里的数据写回到内存,然后再把当前要写入的数据,先从内存读入到 Cache Block 里,然后再把当前要写入的数据写入到 Cache Block,最后也把它标记为脏
  • 如果不是脏的话,把当前要写入的数据先从内存读入到 Cache Block 里,接着将数据写入到这个 Cache Block 里,然后再把这个 Cache Block 标记为脏的就好了。

写回在把数据写入到 Cache 的时候

只有在缓存不命中,同时数据对应的 Cache 中的 Cache Block 为脏标记的情况下

才会将数据写到内存中

而在缓存命中的情况下,则在写入后 Cache 后

只需把该数据对应的 Cache Block 标记为脏即可,而不用写到内存里。


 

这样的好处是,如果我们大量的操作都能够命中缓存,那么大部分时间里 CPU 都不需要读写内存,自然性能相比写直达会高很多。


为什么缓存没命中时,还要定位 Cache Block?这是因为此时是要判断数据即将写入到 cache block 里的位置,是否被「其他数据」占用了此位置,如果这个「其他数据」是脏数据,那么就要帮忙把它写回到内存。


如何理解「缓存不命中 + Cache Block 为脏」

1. 为什么会出现「缓存不命中 + Cache Block 为脏」?

“缓存不命中” 意味着 CPU 要写的数据,当前不在缓存里

而 “Cache Block 为脏” 意味着 这个缓存块里存的是其他数据,且这些数据和内存不一致

(改过没同步)

举个实际例子:

  • 假设 Cache 里有个 Block A,原本存了内存地址 0x100-0x1FF 的数据,你之前修改过它(比如把 0x100 的值从 1 改成 2),所以 Block A 被标记为 “脏”(和内存里旧数据 1 不一致)。
  • 现在 CPU 要写 新数据到内存地址 0x200(这个地址不在当前缓存里,属于 “缓存不命中”),而 Cache 空间不够了,需要找个 Block 替换,刚好选中了 Block A

这时候就触发了:缓存不命中(要写的地址不在缓存) + Cache Block 为脏(Block A 存的旧数据没同步到内存)

所以就出现了,为了让A数据使用B数据这个缓存位置,它把这个位置B数据写入磁盘


2. 写回机制为什么要这么处理?

写回的核心是 “延迟同步内存,尽量用缓存加速

但要保证最终数据不会丢。所以遇到 “缓存不命中 + 脏 Block” 时,必须先处理脏数据,再放新数据,否则旧数据会因为被替换而永远丢失(相当于你临时仓库里改过的东西还没同步到大仓库,就把货架让给新东西,旧数据就没了)。

流程拆解(对应上面例子):

  1. CPU 要写 0x200(缓存不命中),Cache 选 Block A 替换;
  2. 发现 Block A 是 “脏” 的 → 先把 Block A 的数据(改过的 0x100=2)同步写回内存(保证内存最终是最新的);
  3. 把内存地址 0x200 的数据读入 Block A(给新数据腾位置);
  4. 写入新数据到 Block A,标记 Block A 为 “脏”(新数据还没同步到内存)

3. 缓存命中时为什么只标记 “脏”?

缓存命中说明数据本身就在当前 Cache Block 里,直接改缓存里的数据就行,没必要立刻同步内存

写回的逻辑就是 先攒着,等替换时再同步

比如:你要改的 0x100 数据已经在 Block A 里(缓存命中),直接把 Block A 里的值从 1 改成 2,然后标记 “脏”。这样既快(不用等内存),又能保证后续替换时,脏数据会被同步到内存,不会丢


一句话总结

  • 缓存命中:数据就在当前 Block,改缓存 + 标脏 → 延迟同步内存,性能高
  • 缓存不命中 + Block 为脏:要替换的 Block 里有没同步的旧数据 → 先同步旧数据到内存(避免丢失),再加载新数据、标脏 → 保证数据最终一致

这整套逻辑,本质是 “性能” 和 “数据一致性” 的平衡:用 “标脏延迟同步” 换性能,用 “替换时清理脏数据” 保一致。理解后,也能明白为什么写回比写直达(每次写都同步内存)快 —— 大部分写操作不用等内存,只在缓存满、必须替换时才批量同步~


因为写回策略的延迟同步,如果中途出现了故障导致更新丢失没有进入内存该怎么办?

这是无法避免的,像缓存和内存这种是不能完全保证不丢失的,例如Redis

延迟写回是如何降低内存丢失的风险:

  1. MESI协议:多核心场景下,通过协议强制脏块在被其他核心访问前同步。例如:当核心 A 的缓存块是脏的,核心 B 要访问同一块内存时,协议会触发核心 A 先将脏块写回内存,再让核心 B 读取最新数据,避免不一致
  2. 写缓存利用缓冲区和异步进行快速同步:即使采用写回策略,CPU 也会用写缓冲临时存储要写回内存的数据。当脏块被替换时,数据先写入写缓冲(速度极快),再由硬件异步写入内存,减少 CPU 等待时间,同时降低数据丢失概率(写缓冲通常有断电保护的特殊设计,如使用非易失性存储)
  3. 提供特殊指令,允许软件主动将指定缓存块的数据写回内存,强制同步

对于其他的内存操作场景,我们一般会采取以下的策略来尽量减少丢失的数据

  1. 定时同步
  2. 主动同步
  3. mysql的先写日志再写内存和磁盘

简单区分写直达和写回

写直达:

无论如何都会写入内存

命中Cache,则修改Cache再写入内存

不明中Cache,直接写入内存

写回:

缓存不命中的时候修改缓存,将要修改位置的脏数据刷新到内存


缓存一致性

现在 CPU 都是多核的,由于 L1/L2 Cache 是多个核心各自独有的

这会带来多核心的缓存一致性的问题,如果不能保证缓存一致性的问题,就可能造成结果错误


双核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 号核心的缓存,在这个时候是不一致,从而会导致执行结果的错误


使用什么机制可以同步两个不同核心之间的缓存数据

同步机制需要满足的条件

  1. 写传播:某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache
  2. 事务串行化:某个 CPU 核心里对数据的操作顺序必须在其他核心看起来顺序是一样的,这个称为事务的串行化

理解事务串行化

写传播很容易就理解,当某个核心在 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 点:

  1. CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心
  2. 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新

同步机制需要满足的条件

  1. 写传播:某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache
  2. 事务串行化:某个 CPU 核心里对数据的操作顺序必须在其他核心看起来顺序是一样的,这个称为事务的串行化

理解事务串行化

写传播很容易就理解,当某个核心在 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 点:

  1. CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心
  2. 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新

总线嗅探-写传播的实现 

写传播的原则

当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心


写传播最常见实现的方式是总线嗅探

总线嗅探例子:

当 A 号 CPU 核心修改了 L1 Cache 中 i 变量的值,通过总线把这个事件广播通知给其他所有的核心

然后每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面

如果 B 号 CPU 核心的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache


总线嗅探的缺点

总线嗅探方法很简单

CPU 需要每时每刻监听总线上的一切活动

  1. 不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这会加重总线的负载
  1. 总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串行化

于是,有一个协议基于总线嗅探机制实现了事务串行化,也用状态机机制降低了总线带宽压力

这个协议就是 MESI 协议,这个协议就做到了 CPU 缓存一致性


 MESI协议-实现事务的串行化

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 协议的状态图:


MESI流转过程图表


快速复习

CPU Cache的两种数据写入方式:写回,写直达


写直达:把数据同时写入内存和 Cache 中,每次写操作都会写回内存

  • 如果数据已经在 Cache 里面,先将数据更新到 Cache 里面,再写入到内存里面;
  • 如果数据没有在 Cache 里面,就直接把数据更新到内存里面(相当于 “绕开缓存,直接存硬盘”)

写回(减少读写内存的频率 提高系统性能):

核心逻辑:写操作只更新缓存内存延迟到 “缓存块被替换 / 清理时” 才同步

写回的逻辑:先攒着,等替换时再同步

缓存不命中的时候修改缓存,将要修改位置的脏数据刷新到内存

新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」时才需要写到内存中

写入流程:只有在缓存不命中,同时数据对应的 Cache 中的 Cache Block 为脏标记的情况下,才会将数据写到内存中

通过命中缓存减少CPU读写内存的次数


如何理解【缓存不命中】+【Cache Block为脏】:

写回的逻辑就是 先攒着,等替换时再同步
 

Cache Block 是缓存的最小存储和传输单位,缓存是一个由 Cache Block 组成的集合

我们是根据内存地址映射去查找缓存的,查找缓存的时候不只要查找缓存里面是否有数据,还要查找这个缓存是否是我们当前内存地址的缓存

Tag:Cache有一个组标记Tag字段来进行标记的

标记位:还要查看标记位,0表示缓存无效,1表示缓存有效

两者同时出现的情况:当 CPU 要写新数据到一个不在缓存中的地址时,会发生缓存不命中。如果此时缓存空间已满,需要替换掉一个 Cache Block,而被选中的 Cache Block 恰好是之前被修改过且未同步到主存的,那么它就是脏的,这样就会同时出现缓存不命中和 Cache Block 为脏的情况


缓存命中时:说明数据本身就在当前 Cache Block 里,直接改缓存里的数据就行,没必要立刻同步内存

缓存不命中时:从内存中读取数据并将数据写入缓存中

缓存不命中时+Cache Block为脏:从内存中读取数据并将数据写入缓存中,但此时缓存中有旧数据,要将旧数据刷入内存中


延迟写回是如何降低内存丢失风险的:

  1. MESI协议
  2. 写缓存利用缓冲区和异步进行快速同步
  3. 提供特殊指令,主动将数据刷入内存

对于其他的内存操作场景,我们一般会采取以下的策略来尽量减少丢失的数据

  1. 定时同步
  2. 主动同步
  3. WLA技术,mysql的先写日志再写内存和磁盘

什么是缓存一致性问题:

多个CPU操作共享变量,在多线程情况下的丢失修改,和数据不统一,数据不同步问题


如何同步两个不同核心之间的缓存数据:

  1. 写传播:某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache
  2. 事务串行化:某个 CPU 核心里对数据的操作顺序必须在其他核心看起来顺序是一样的,这个称为事务的串行化

实现事务串行化的条件:

  1. CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心
  2. 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新

总线嗅探-写传播的实现:

  1. 总线嗅探方法:每时每刻监听总线上的一切活动,当监听到CPU修改共享变量时,通过总线把这个事件广播通知给其他所有的核心

总线嗅探的缺点:

  1. 不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这会加重总线的负载
  2. 总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串行化

MESI协议-实现事务的串行化:

MESI的4个状态:

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

MESI4个状态的流转:

  1. A读取变量i后,A独占变量i
  2. B读取变量i后,AB共享变量i
  3. A修改变量i,B的变量i变为已失效,A的变量i变成已修改
http://www.dtcms.com/a/263925.html

相关文章:

  • AI智能体在用户行为数据分析中有哪些应用?
  • 具身多模态大模型在感知与交互方面的综述
  • (十一)Spring WebSocket
  • Ansys Speos | Speos Camera 传感器机器视觉示例
  • vue-35(使用 Jest 和 Vue Test Utils 设置测试环境)
  • 列表元素滚动动画
  • LAN8720 寄存器概览和STM32 HAL库读写测试
  • CSS 安装使用教程
  • FreeRTOS任务切换
  • 力扣网C语言编程题:寻找两个正序数组的中位数
  • RIP 技术深度解析
  • 文心一言开源版测评:能力、易用性与价值的全面解析
  • [创业之路-457]:企业经营层 - 蓝海战略 - 价值创新不仅仅是技术创新
  • Java项目:基于SSM框架实现的智慧养老平台管理系统【ssm+B/S架构+源码+数据库+毕业论文】
  • 大麦基于HarmonyOS星盾安全架构,打造全链路安全抢票方案
  • 【机器学习深度学习】模型微调的基本概念与流程
  • 06会话管理
  • 前端计算机视觉:使用 OpenCV.js 在浏览器中实现图像处理
  • 量化选股策略 聚宽
  • 如何利用Charles中文版抓包工具提升API调试与网络性能
  • 二刷 苍穹外卖day10(含bug修改)
  • 如何使用StartUML绘制类图,用例图,时序图入门
  • 转录组分析流程(二):差异分析
  • MySQL MVCC 详解
  • ChatGPT使用限额记录与插件统计
  • 杭州来未来科技 Java 实习面经
  • [C#] WPF - 自定义样式(Slider篇)
  • 【Hive SQL优化完全指南:从0.x到4.x的性能进化之路】
  • c# IO密集型与CPU密集型任务详解,以及在异步编程中的使用示例
  • [2025CVPR]DE-GANs:一种高效的生成对抗网络