首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Intel DPDK缓存一致性介绍

Intel DPDK缓存一致性介绍

作者头像
通信行业搬砖工
发布2023-12-20 13:47:52
发布2023-12-20 13:47:52
4750
举报
文章被收录于专栏:网络虚拟化网络虚拟化

本文章由美团数据面大佬发表于知乎:

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

如有侵权,请联系删除文章,谢谢!

了解更多欢迎访问知乎 :https://www.zhihu.com/people/mu-mu-67-87-35


1 Cache系统架构

之前我们介绍过缓存系统,今天我们就其中的缓存一致性进阶展开讨论一番,下图是Cache系统逻辑示意图:

之前我们讨论过Cache预取的原理,Cache之所以能提高系统性能,无外乎程序的执行存在局部性现象,时间局部性和空间局部性。

  • 时间局部性:指程序即将用到的指令/数据可能就是目前正在使用的指令/数据。因此,当前用到的指令/数据在使用完毕之后可以暂时存放在Cache中,可以在将来的时候再被处理器使用到。比如一个循环指令,在循环终止前,处理器需要反复执行循环语句中的指令。
  • 空间局部性:这部分更好理解,程序即将用到的指令/数据可能与目前正在用到的指令/数据存在空间相邻和相近,没错,最简单的例子就是一个需要顺序处理的数组了,这样处理器在处理当前的指令/数组时可以把相邻的指令/数组读取到Cache中,节省了访问内存的时间。

可见缓存是整个存储体系的核心。

2 缓存原理

2.1 缓存行

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如何在缓存中定位?也就是找到他的组号先:

代码语言:javascript
复制
Set Index = (addr >> 6) % M;

右移6位是因为Block Index占addr的低六位,Data为64字节。

遍历该组所有的路,找到cache line中的Tag与addr中Tag相等为止,所有路都没有匹配成功,那么缓存未命中。

代码语言:javascript
复制
整个缓存容量 = 组数 × 路数 × 缓存行大小

举个例子,来看我设备的cpu信息:

代码语言:javascript
复制
[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

缓存信息:

代码语言:javascript
复制
[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

通过缓存行大小和路数可以倒推出缓存的组数:

代码语言:javascript
复制
缓存组数 = 整个缓存容量 ÷ 路数 ÷ 缓存行大小

2.2 伪共享(False Sharing)

于局部性原理的应用,CPU Cache 在读取内存数据时,每次不会只读一个字或一个字节,而是一块块地读取,每一小块数据也叫 CPU 缓存行(CPU Cache Line)。

在并行场景中,当多个处理器核心修改同一个缓存行变量时,有 2 种情况:

  • 情况 1 - 修改同一个变量: 两个处理器并行修改同一个变量的情况,CPU 会通过 MESI 机制维持两个核心的缓存中的数据一致性(Conherence)。简单来说,一个核心在修改数据时,需要先向所有核心广播 RFO 请求,将其它核心的 Cache Line 置为 “已失效”。其它核心在读取或写入 “已失效” 数据时,需要先将其它核心 “已修改” 的数据写回内存,再从内存读取;

事实上,多个核心修改同一个变量时,使用 MESI 机制维护数据一致性是必要且合理的。但是多个核心分别访问不同变量时,MESI 机制却会出现不符合预期的性能问题。

  • 情况 2 - 修改不同变量: 两个处理器并行修改不同变量的情况,从程序员的逻辑上看,两个核心没有数据依赖关系,因此每次写入操作并不需要把其他核心的 Cache Line 置为 “已失效”。但从 CPU 的缓存一致性机制上看,由于 CPU 缓存的颗粒度是一个个缓存行,而不是其中的一个个变量。当修改其中的一个变量后,缓存控制机制也必须把其它核心的整个 Cache Line 置为 “已失效”。

在高并发的场景下,核心的写入操作就会交替地把其它核心的 Cache Line 置为失效,强制对方刷新缓存数据,导致缓存行失去作用,甚至性能比串行计算还要低。

这个问题我们就称为伪共享问题,出现伪共享问题时,有可能出现程序并行执行的耗时比串行执行的耗时还要长。耗时排序:并行执行有伪共享 > 串行执行 > 并行执行无伪共享。

3 缓存一致性协议

在单核时代,增加缓存可以大大提高读写速度,但是到了多核时代,却引入了缓存一致性问题,如果有一个核心修改了缓存行中的某个值,那么必须有一种机制保证其他核心能够观察到这个修改。

3.1 MESI

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-

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 通信行业搬砖工 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本文章由美团数据面大佬发表于知乎:
  • 如有侵权,请联系删除文章,谢谢!
  • 1 Cache系统架构
  • 2 缓存原理
    • 2.1 缓存行
    • 2.2 伪共享(False Sharing)
  • 3 缓存一致性协议
    • 3.1 MESI
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档