cache的基本原理 和 多核cache的一致性

原文转载自知乎:
cache 的基本原理
多核Cache一致性
10 张图打开 CPU 缓存一致性的大门

另外一本很不错的书籍:
Memory Barriers: a Hardware View for Software Hackers,强烈建议阅读。

为什么需要cache?

Latency Numbers Every Programmer Should Know

image.png
cache对性能的影响

cache的组织

image.png

L1 cache 分为单独的 instruction cache(ICache)和 data cache(DCache)。
L1 cache是CPU私有的,每个CPU都有一个L1 cache。一个cluster 内的所有CPU共享一个L2 cache,L2 cache不区分指令和数据,都可以缓存。所有cluster之间共享L3 cache。L3 cache通过总线和主存相连。

  • 查看CPU L1,L2,L3 大小
    lscpu | grep cache
    
多级cache之间的配合工作

当CPU试图从某地址load数据时,首先从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中缓存。

直接映射缓存(Direct mapped cache)
  • cache line
    cache line是 cache 和主存之间数据传输的最小单位。当CPU 试图 load 一个字节数据的时候,如果 cache 缺失,那么 cache 控制器会从主存中一次性的 load cache line 大小的数据到 cache 中。例如,cache line 大小是8字节。CPU即使读取一个byte,在 cache 缺失后,cache 会从主存中 load 8 字节填充整个 cache line。
    # cat / proc/cpuinfo | grep "cache_alignment"  # 查看当前cache line大小
    
image.png
两路组相连缓存(Two-way set associative cache)

TODO

全相连缓存(Full associative cache)

TODO


cache 的分配策略

cache的分配策略是指我们什么情况下应该为数据分配 cache line。cache分配策略分为读和写两种情况。

读分配

当CPU读数据时,发生cache缺失,这种情况下都会分配一个cache line缓存从主存读取的数据。默认情况下,cache都支持读分配。

写分配

Cache更新策略(Cache update policy)

cache更新策略是指当发生cache命中时,写操作应该如何更新数据。cache更新策略分成两种:

  • 写直通
  • 写回
写直通(write through)

当CPU执行store指令并在cache命中时,我们更新cache中的数据并且更新主存中的数据。cache和主存的数据始终保持一致。

写回(write back)

当CPU执行store指令并在cache命中时,我们只更新cache中的数据。并且每个cache line中会有一个bit位记录数据是否被修改过,称之为dirty bit(翻翻前面的图片,cache line旁边有一个D就是dirty bit)。我们会将dirty bit置位。主存中的数据只会在cache line被替换或者显示的clean操作时更新。因此,主存中的数据可能是未修改的数据,而修改的数据躺在cache中。cache和主存的数据可能不一致。


多核心cache的一致性

在多核系统(简称MP,Multi-Processor),我们知道每个CPU都有一个私有的L1 Cache(不细分iCache和dCache)。
假设一个双核的系统,我们将会有2个L1 Cache。这就引入了一个问题,不同CPU之间的 L1 Cache 如何保证一致性呢?首先看下什么是多核Cache一致性问题。

假设2个CPU的系统,并且 L1 Cache 的 cache line 大小是64 Bytes。两个CPU都读取 0x00000040 地址数据,导致 0x00000040 开始的64 B字节内容分别加载到CPU0和CPU1的私有的cache line。

image.png

CPU0执行写操作,写入值0x01,CPU0私有的 L1 Cache 更新 cache line 的值,然后,CPU1读取0x40数据,CPU1发现命中cache,然后返回0x00值,并不是CPU0写入的0x01。这就造成了 CPU0 和 CPU1 私有L1 Cache数据不一致现象。

image.png

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

  • 某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Wreite Propagation);
  • 某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串形化(Transaction Serialization)。

第一点写传播很容易就理解,当某个核心在 Cache 更新了数据,就需要同步到其他核心的 Cache 里。而对于第二点事务事的串形化,我们举个例子来理解它。

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

image.png

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 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。

现在,再让我们看看写传播和事务串形化具体是用什么技术实现的。

Lock 指令

在早期的CPU当中,是通过在总线上加LOCK锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加 LOCK 锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。在总线上发出了LCOK 锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从其内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是由于在锁住总线期间,其他CPU无法访问内存,会导致效率低下。

Bus Snooping Protocol

写传播的原则就是当某个 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 缓存一致性。


image.png
MESI Protocol

MESI是现在一种使用广泛的协议,用来维护多核Cache一致性。可以将MESI看做是状态机,将每一个cache line标记状态,并且维护状态的切换。其大概思想就是:
一个Cache加载一个变量的时候,是Exclusive状态,当这个变量被第二个Cache加载,更改状态为Shared;这时候一个CPU要修改变量, 就把状态改为Modified,并且Invalidate其他的Cache,其他的Cache再去读这个变量,达到一致。

  • MESI 协议中的 cache line 的四种状态

    状态名称 定义 备注
    invalid cache line 没有被使用
    shared cache line 对应的主从数据块可能也在非当前CPU的cache line 中 在未通知其他cpu之前,不能修改该cache line 相当于只读
    modifyed 该 cache line 对应的主存数据块只存在当前的 cpu cache 中,且已被当前cpu修改,从而最新的数据只在该cache line 中 当前 cpu 在使用该cache line 存储别的数据之前,必须要将当前的数据写回主存或者转交其他CPU
    exclusive 该cache line 对应的数据块只存在当前的 cpu cache 中,当前 CPU 还未修改该 cache line,因此该 cache line 和主存都是最新数据 当前cpu在使用该 cache line 存储别的数据时,可以丢弃当前当期数据,即无需写回主存也无需转交其他CPU
  • MESI 协议中的 cache line 的六种消息

    消息名称 定义
    Read 如果CPU需要读取某个地址的数据
    Read Response 可以由主存或者其他CPU发出,包含"Read"消息请求的主存地址的数据,当发出"Read"消息的CPU收到该消息后,将"Read Response"中的数据 放入自身的 cache line
    Invalidate 其中包含待失效的主存地址,其他CPU收到消息后将某地址对应的 cache line 移除,并将返回"Invalidate Acknowledge"
    Invalidate Acknowledge 在收到 Invalidate 消息后,必须移除对应的cache line,并返回"Invalidate Acknowledge"
    Read Invalidate Read + Invalidate消息的结合
    Writeback 包含主存和待写回数据,该消息仅在cache line 为 modified 状态发出,该消息之后,对应的 cache line 如需要可以被移除,从而使其可以被用于存储别的主存块
image.png

状态之间的相互转换关系可以使用下表来表示:

image.png
image.png

继续上面的例子:

  • 当CPU0读取0x40数据,数据被缓存到CPU0私有Cache,此时CPU1没有缓存0x40数据,所以我们标记 cache line 状态为 Exclusive
    Exclusive代表cache line对应的数据仅在数据只在一个CPU的私有Cache中缓存,并且其在缓存中的内容与主存的内容一致。

  • 然后CPU1读取0x40数据,发送消息给其他CPU,发现数据被缓存到CPU0私有Cache,数据从CPU0 Cache返回给CPU1。此时CPU0和CPU1同时缓存0x40数据,此时cache line状态从Exclusive切换到Shared状态。
    Shared代表cache line对应的数据在"多"个CPU私有Cache中被缓存,并且其在缓存中的内容与主存的内容一致。

  • 继续CPU0修改0x40地址数据,发现0x40内容所在cache line状态是Shared。CPU0发出invalid消息传递到其他CPU,这里是CPU1。CPU1接收到invalid消息。将0x40所在的cache line置为Invalid状态。
    Invalid状态表示表明当前cache line无效。

    然后 CPU0 收到 CPU1 已经invalid的消息,修改0x40所在的cache line中数据。并更新cache line状态为Modified
    Modified表明cache line对应的数据仅在一个CPU私有Cache中被缓存,并且其在缓存中的内容与主存的内容不一致,代表数据被修改。

  • 如果CPU0继续修改0x40数据,此时发现其对应的cache line的状态是Modified。因此CPU0不需要向其他CPU发送消息,直接更新数据即可。

  • 如果0x40所在的cache line需要替换,发现cache line状态是Modified。所以数据应该先写回主存。

Cache 之间数据和状态同步沟通,是通过发送message 来完成的。MESI主要涉及一下几种message。

  • Read: 如果CPU需要读取某个地址的数据。
  • Read Response: 答复一个Read消息,并且返回需要读取的数据。
  • Invalidate: 请求其他CPU将某地址对应的 cache line 移除。
  • Invalidate Acknowledge: 回复 invalidate 消息,表明对应的 cache line已经被移除。
  • Read Invalidate: Read + Invalidate消息的组合。
  • Writeback: 该消息包含要回写到内存的地址和数据。

继续以上的例子,我们有5个步骤。现在加上这些message,看看消息是怎么传递的。

  • CPU0 发出 Read 消息。主存返回 Read Response 消息,消息包含地址0x40的数据。
  • CPU1 发出 Read 消息,CPU0返回 Read Response 消息,消息包含地址 0x40 数据。
  • CPU0 发出 Invalidate 消息,CPU1接到消息后,返回Invalidate Acknowledge 消息。
  • 不需要发送任何消息。
  • 发送Writeback消息。

你可能感兴趣的:(cache的基本原理 和 多核cache的一致性)