原文转载自知乎:
cache 的基本原理
多核Cache一致性
10 张图打开 CPU 缓存一致性的大门
另外一本很不错的书籍:
Memory Barriers: a Hardware View for Software Hackers,强烈建议阅读。
为什么需要cache?
Latency Numbers Every Programmer Should Know
cache对性能的影响
cache的组织
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大小
两路组相连缓存(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。
CPU0执行写操作,写入值0x01,CPU0私有的 L1 Cache 更新 cache line 的值,然后,CPU1读取0x40数据,CPU1发现命中cache,然后返回0x00值,并不是CPU0写入的0x01。这就造成了 CPU0 和 CPU1 私有L1 Cache数据不一致现象。
那么,要解决这一问题,就需要一种机制,来同步两个不同核心里面的缓存数据。要实现的这个机制的话,要保证做到下面这 2 点:
- 某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Wreite 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 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。
现在,再让我们看看写传播和事务串形化具体是用什么技术实现的。
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 缓存一致性。
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 如需要可以被移除,从而使其可以被用于存储别的主存块
状态之间的相互转换关系可以使用下表来表示:
继续上面的例子:
当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消息。