https://zhuanlan.zhihu.com/p/657085678

了解更多欢迎访问知乎 :https://www.zhihu.com/people/mu-mu-67-87-35
之前我们介绍过缓存系统,今天我们就其中的缓存一致性进阶展开讨论一番,下图是Cache系统逻辑示意图:

之前我们讨论过Cache预取的原理,Cache之所以能提高系统性能,无外乎程序的执行存在局部性现象,时间局部性和空间局部性。
可见缓存是整个存储体系的核心。
Cache是由很多个 Cache line 组成的。cache line(缓存行)是缓存进行管理的最小存储单元,也叫缓存块,每个 cache line 包含Flag、Tag和Data,不同型号CPU的Flag和Tag可能不同,Cache line 是 cache 和 RAM 交换数据的最小单位,通常为 64 Byte。当 CPU 把内存的数据载入 cache 时,会把临近的共 64 Byte 的数据一同放入同一个Cache line,因为空间局部性:临近的数据在将来被访问的可能性大。

缓存是按照矩阵方式排列(M × N),横向是组(Set),纵向是路(Way)。每一个元素是缓存行(cache line)。给定一个虚拟地址addr如何在缓存中定位?也就是找到他的组号先:
Set Index = (addr >> 6) % M;右移6位是因为Block Index占addr的低六位,Data为64字节。
遍历该组所有的路,找到cache line中的Tag与addr中Tag相等为止,所有路都没有匹配成功,那么缓存未命中。
整个缓存容量 = 组数 × 路数 × 缓存行大小举个例子,来看我设备的cpu信息:
[root@mumu]# lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 32
On-line CPU(s) list: 0-31
Thread(s) per core: 1
Core(s) per socket: 16
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 106
Model name: Intel(R) Xeon(R) Silver 4314 CPU @ 2.40GHz
Stepping: 6
CPU MHz: 3400.000
CPU max MHz: 3400.0000
CPU min MHz: 800.0000
BogoMIPS: 4800.00
Virtualization: VT-x
L1d cache: 48K
L1i cache: 32K
L2 cache: 1280K
L3 cache: 24576K
NUMA node0 CPU(s): 0-15
NUMA node1 CPU(s): 16-31
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 intel_pt ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm rdt_a avx512f avx512dq rdseed adx smap avx512ifma clflushopt clwb avx512cd sha_ni avx512bw avx512vl xsaveopt xsavec xgetbv1 cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts avx512vbmi pku ospke avx512_vpopcntdq spec_ctrl intel_stibp flush_l1d arch_capabilities缓存信息:
[root@mumi ~]# getconf -a | grep CACHE
LEVEL1_ICACHE_SIZE 32768 //L1i 缓存大小 32K
LEVEL1_ICACHE_ASSOC 8 //L1i 路数
LEVEL1_ICACHE_LINESIZE 64 //L1i 缓存行大小 64B
LEVEL1_DCACHE_SIZE 49152 //L1d 缓存大小 48K
LEVEL1_DCACHE_ASSOC 12 //L1d 路数
LEVEL1_DCACHE_LINESIZE 64 //L1d 缓存行大小 64B
LEVEL2_CACHE_SIZE 1310720 //L2 缓存大小 1280K
LEVEL2_CACHE_ASSOC 20 //L2 路数
LEVEL2_CACHE_LINESIZE 64 //L2 缓存行大小 64B
LEVEL3_CACHE_SIZE 25165824 //L3 缓存大小 24M
LEVEL3_CACHE_ASSOC 12 //L3 路数
LEVEL3_CACHE_LINESIZE 64 //L3 缓存行大小 64B
LEVEL4_CACHE_SIZE 0
LEVEL4_CACHE_ASSOC 0
LEVEL4_CACHE_LINESIZE 0
通过缓存行大小和路数可以倒推出缓存的组数:
缓存组数 = 整个缓存容量 ÷ 路数 ÷ 缓存行大小于局部性原理的应用,CPU Cache 在读取内存数据时,每次不会只读一个字或一个字节,而是一块块地读取,每一小块数据也叫 CPU 缓存行(CPU Cache Line)。
在并行场景中,当多个处理器核心修改同一个缓存行变量时,有 2 种情况:
事实上,多个核心修改同一个变量时,使用 MESI 机制维护数据一致性是必要且合理的。但是多个核心分别访问不同变量时,MESI 机制却会出现不符合预期的性能问题。
在高并发的场景下,核心的写入操作就会交替地把其它核心的 Cache Line 置为失效,强制对方刷新缓存数据,导致缓存行失去作用,甚至性能比串行计算还要低。
这个问题我们就称为伪共享问题,出现伪共享问题时,有可能出现程序并行执行的耗时比串行执行的耗时还要长。耗时排序:并行执行有伪共享 > 串行执行 > 并行执行无伪共享。

在单核时代,增加缓存可以大大提高读写速度,但是到了多核时代,却引入了缓存一致性问题,如果有一个核心修改了缓存行中的某个值,那么必须有一种机制保证其他核心能够观察到这个修改。
MESI协议缓存状态
MESI是Modify(修改)、Exclusive(独享、互斥)、Shared(共享)、Invalid(无效)首字母组成的。用于对单个缓存行的数据进行加锁,不会影响到内存中其他数据的读写。
状态 | 描述 | 监听任务 |
|---|---|---|
M 修改 (Modified) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
E 独享、互斥 (Exclusive) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
S 共享 (Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
I 无效 (Invalid) | 该Cache line无效。 | 无 |
任意一对缓存,对应缓存行的相容关系:

当块标记为 M (已修改), 在其他缓存中的数据副本被标记为I(无效)。
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。
从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。
MESI状态转换

在理解该图之前先来看:
1.触发事件
触发事件 | 描述 |
|---|---|
本地读取(Local read) | 本地cache读取本地cache数据 |
本地写入(Local write) | 本地cache写入本地cache数据 |
远端读取(Remote read) | 其他cache读取本地cache数据 |
远端写入(Remote write) | 其他cache写入本地cache数据 |
2.cache分类
前提:所有的cache共同缓存了主内存中的某一条数据。
本地cache:指当前cpu的cache。 触发cache:触发读写事件的cache。 其他cache:指既除了以上两种之外的cache。 注意:本地的事件触发 本地cache和触发cache为相同。
上图的切换解释:
状态 | 触发本地读取 | 触发本地写入 | 触发远端读取 | 触发远端写入 |
|---|---|---|---|---|
M状态(修改) | 本地cache:M触发cache:M其他cache:I | 本地cache:M触发cache:M其他cache:I | 本地cache:M→E→S触发cache:I→S其他cache:I→S同步主内存后修改为E独享,同步触发、其他cache后本地、触发、其他cache修改为S共享 | 本地cache:M→E→S→I触发cache:I→S→E→M其他cache:I→S→I同步和读取一样,同步完成后触发cache改为M,本地、其他cache改为I |
E状态(独享) | 本地cache:E触发cache:E其他cache:I | 本地cache:E→M触发cache:E→M其他cache:I本地cache变更为M,其他cache状态应当是I(无效) | 本地cache:E→S触发cache:I→S其他cache:I→S当其他cache要读取该数据时,其他、触发、本地cache都被设置为S(共享) | 本地cache:E→S→I触发cache:I→S→E→M其他cache:I→S→I当触发cache修改本地cache独享数据时时,将本地、触发、其他cache修改为S共享.然后触发cache修改为独享,其他、本地cache修改为I(无效),触发cache再修改为M |
S状态(共享) | 本地cache:S触发cache:S其他cache:S | 本地cache:S→E→M触发cache:S→E→M其他cache:S→I当本地cache修改时,将本地cache修改为E,其他cache修改为I,然后再将本地cache为M状态 | 本地cache:S触发cache:S其他cache:S | 本地cache:S→I触发cache:S→E→M其他cache:S→I当触发cache要修改本地共享数据时,触发cache修改为E(独享),本地、其他cache修改为I(无效),触发cache再次修改为M(修改) |
I状态(无效) | 本地cache:I→S或者I→E触发cache:I→S或者I →E其他cache:E、M、I→S、I本地、触发cache将从I无效修改为S共享或者E独享,其他cache将从E、M、I 变为S或者I | 本地cache:I→S→E→M触发cache:I→S→E→M其他cache:M、E、S→S→I | 既然是本cache是I,其他cache操作与它无关 | 既然是本cache是I,其他cache操作与它无关 |
下图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态
M | E | S | I | |
|---|---|---|---|---|
M | × | × | × | √ |
E | × | × | × | √ |
S | × | × | √ | √ |
I | √ | √ | √ | √ |
举个例子来说:
假设cache 1 中有一个变量x = 0的cache line 处于S状态(共享)。 那么其他拥有x变量的cache 2、cache 3等x的cache line调整为S状态(共享)或者调整为 I 状态(无效)。

-END-