问题 ①:CPU 与内存读写速度差距大,直接与内存交互影响 CPU 性能。
解决方法:在 CPU 与内存之间插入多级缓存。
CPU 访问内存数据时,拷贝一块连续的“缓存行”到 cache 中,再操作 cache 中的数据;cache 中的数据再在需要时同步回内存。(扩展:存在 “伪共享问题”)
问题 ②:单核 CPU 性能发展遇到瓶颈。
解决办法:多核 CPU 并行工作。
为减少缓存行冲突,提高缓存命中率,多核 CPU 都拥有独立的 cache。(不同线程通常执行不同代码,访问的内存数据不聚集,共用缓存会经常无法命中)
问题 ③:对于同一份内存数据,当有 CPU 在修改时,该数据在不同 CPU 缓存的值可能不一致。
解决方法:总线锁。
限制同一时间内,只有一个 CPU 能访问内存数据。
问题 ④:总线锁开销太大,且仍需额外解决缓存的冲突。
解决办法:MESI 缓存一致性协议。
核心思想:通过降低锁的粒度,减少使用总线锁的频率,从而达到性能优化。
MESI 协议维护一个有限状态机,对每个 CPU 拷贝的每个缓存行都赋予一个状态属性;当感知到对某个缓存行的读写事件发生时,根据该缓存行的状态,对其进行一致性处理。
CPU 通过 “总线嗅探(Bus snooping)” 来感知其他 CPU 读写事件的发生:每个缓存中包含一个监视器(snooper),监听器会监视总线上的每个事务。如果总线上出现修改共享缓存行的事务,则所有监听器都会检查其缓存行是否具有该共享缓存行的相同副本。
MESI 协议对每个缓存行赋予一个状态属性,MESI 协议就是由这四种状态的首字母来命名的:
由于只有 4 种状态,可以只用 2 bit 进行编码。
对单个缓存行,根据缓存数据与内存数据是否一致,还可分为两种状态:
“脏的” 与 “干净的” 只是为了便于理解而划分的状态,能与 MESI 四个状态相对应,不会消耗 bit 去记录。
Modified 状态表示缓存行数据已被当前 CPU 修改,且该缓存行是“脏的”,此时缓存行的数据是最新的。
Modified 状态的缓存行仅被当前 CPU 独占式缓存。
Exclusive 状态表示缓存行仅被当前 CPU 独占式缓存,且该缓存行是“干净的”。
Exclusive 状态是对 Shared 状态的一种优化,目的是在写入时减少发布一次 BusUpgr 总线事件。
Shared 状态表示缓存行 可能 被其他 CPU 同时缓存。该缓存行是“干净的”。
Shared 状态不是精确的,当其他 CPU 丢弃了共享的缓存行,此时当前缓存行为独占缓存,但不会变为 Exclusive 状态。
Invalid 状态表示缓存行已失效。该缓存行是“脏的”,此时内存的数据是最新的。
Invalid 状态可以视作没有缓存该数据,可以忽略其他 CPU 的读写事件。
引起缓存行状态转换的事件可分为两类:
此外,还有一些衍生处理事件,在某些事件发生时辅助缓存行进行状态更新。
当前 CPU 对该缓存行进行读取。
缓存命中时,不会改变缓存行状态;缓存未命中时,根据是否已经被其他 CPU 缓存,设置为 Shared 或 Exclusive 状态。
当前 CPU 对该缓存行进行修改。
会将缓存行设置为 Modified 状态。
发布 BusUpgr 或 BusRdX 获取独占式缓存的行为,又称为 RFO(Request For Ownership)操作。当 CPU 试图写入 Shared / Invalid 状态的缓存行时,会发出此操作,将所有其他 CPU 的该行缓存状态设置为 Invalid。
其他 CPU 请求读取与该缓存行相同的内存数据。
其他 CPU 请求写入与该缓存行相同的内存数据,且该数据尚未被待写入的 CPU 缓存。
该事件目的是获取该数据的独占缓存权,为后续的写入操作铺平道路。
BusRdX 中的 X 即是“独占(Exclusive)”的意思,Rd 表示要先将数据缓存到 cache,之后再对缓存进行修改。
其他 CPU 请求写入与该缓存行相同的内存数据,且该数据已经被待写入的 CPU 缓存。
BusUpgr 中的 Upgr 即是“升级(Upgrade)”的意思,将共享缓存转换为独占缓存,以便后续对缓存进行修改。
将缓存行强制写回内存中。
将缓存行发布到总线上,通过总线从一个 CPU 的 cache 直接同步到另一个 CPU 的 cache,而不是先写回内存再从内存读取。
内存也可以设置监听器,从总线上监听 FlushOpt 事件、获取缓存行并更新内存。
这使得 “更新其他 CPU 的缓存行” 与 “更新内存” 从串行改为并行,提高了性能。
stateDiagram-v2
Modified --> Modified : PrRd / PrWr
Modified --> Shared : BusRd
Modified --> Invalid : BusRdX
本地 CPU,对 Modified 状态缓存行,发起读取请求:
本地 CPU,对 Modified 状态缓存行,发起写入请求:
其他 CPU,对与该 Modified 缓存行相同的内存数据,发起读取请求:
其他 CPU,对与该 Modified 缓存行相同的内存数据,发起写入请求:
stateDiagram-v2
Exclusive --> Exclusive : PrRd
Exclusive --> Modified : PrWr
Exclusive --> Shared : BusRd
Exclusive --> Invalid : BusRdX
本地 CPU,对 Exclusive 状态缓存行,发起读取请求:
本地 CPU,对 Exclusive 状态缓存行,发起写入请求:
其他 CPU,对与该 Exclusive 缓存行相同的内存数据,发起读取请求:
其他 CPU,对与该 Exclusive 缓存行相同的内存数据,发起写入请求:
stateDiagram-v2
Shared --> Shared : PrRd / BusRd
Shared --> Modified : PrWr
Shared --> Invalid : BusRdX / BusUpgr
本地 CPU,对 Shared 状态缓存行,发起读取请求:
本地 CPU,对 Shared 状态缓存行,发起写入请求:
其他 CPU,对与该 Shared 缓存行相同的内存数据,发起读取请求:
其他 CPU,对与该 Shared 缓存行相同的内存数据,发起写入请求:
stateDiagram-v2
Invalid --> Shared/Exclusive : PrRd
Invalid --> Modified : PrWr
Invalid --> Invalid : BusRd / BusRdX / BusUpgr
本地 CPU,对 Invalid 状态缓存行,发起读取请求:
本地 CPU,对 Invalid 状态缓存行,发起写入请求:
其他 CPU,对与该 Invalid 缓存行相同的内存数据,发起读取请求:
其他 CPU,对与该 Invalid 缓存行相同的内存数据,发起写入请求:
MESI 协议的最大性能开销源于写入前的 RFO 操作,该操作负责获取独占式缓存(仅在命中 Shared 或 Invalid 时触发)。
RFO 操作会发出 BusRdX 或 BusUpgr 事件,并执行以下等待:
主要的优化方式有两种:写缓冲区、无效化队列。
这两种优化结合,能大幅提高 CPU 性能。但会失去缓存状态的强一致性,取而代之的是最终一致性,会带来内存重排的问题。
写缓冲区(Store Buffer)的作用是将 RFO 操作异步化,CPU 无需等待 RFO 完成即可继续执行后续指令。
这是在写入操作的当前 CPU 端进行的优化,即对 PrWr 事件处理的优化。·
写缓冲区的工作流程如下:
可将写缓冲区理解为 “待处理写入任务表”,其大小有限(通常 4~64 条目),若持续写入超出容量,CPU 仍需阻塞。
写缓冲区具有写入合并(Write Combining)特性:在 RFO 操作期间,若该缓存行发生了多次写入,写缓冲区会将多次写入合并为同一条目,不会重复触发 RFO 操作。
若对数据的写入操作暂存到写缓冲区,RFO 正在异步执行时,对该数据的读取可能会有如下情况:
写缓冲区优化后,会先执行后续操作,RFO 完成才真正写入缓存,才能被其他 CPU 获取到这次写入。这相当于把前面的 Store 操作移到任意后续操作之后,可能造成 StoreStore 或 StoreLoad 重排。
X86 架构的 CPU 强制写缓冲区按程序顺序提交写入操作,即使后续写入操作命中缓存,也要等待前面的写入 RFO 操作完成后再开始处理。
写屏障(store barrier)会刷新写缓冲区,强制等待所有写操作都已应用到当前 CPU 的缓存中后,才能执行后续指令。确保屏障之前的写操作,不会被重排到屏障后面(必然写入到内存)。
写屏障不能保证其他 CPU 一定对当前写入后的值立即可见,因为其他 CPU 存在无效化队列,在未处理无效事件时仍会访问到旧值。
无效化队列(Invalidate Queues)的作用是减少其他 CPU 发起的 RFO 操作中,等待缓存副本失效的时间。
这是在写入操作的其他 CPU 端进行的优化,即对 BusRdX 或 BusUpgr 事件处理的优化。·
CPU 在收到 BusRdX 或 BusUpgr 事件时,无优化情况下会先将缓存行转换为 Invalid,状态转换完成后才返回 ACK 确认。在使用无效化队列优化时,工作流程如下:
无效化队列也会合并多个 BusRdX 或 BusUpgr 事件,当作一个无效事件进行处理。
若缓存行的无效事件已存入无效化队列,对该缓存的读写操作根据不同 CPU 架构有不同的行为:
无效化队列优化后,读取到的值可能是个旧值。相当于把后面的 Load 操作移到任意操作之前(读取之前的值),可能造成 LoadLoad、StoreLoad 重排。
X86 架构每次读写前强制处理无效事件,所以无效队列在该架构不会造成重排。
读屏障(read barrier)会刷新无效化队列,强制等待缓存更新后才能执行后续指令,从而确保其他 CPU 的写操作对当前 CPU 可见。确保屏障之后的读操作,不会被重排到屏障前面(必然读取到最新值)。
写屏障和读屏障不等同于 Release 和 Acquire 语义。Release 和 Acquire 语义还需要处理配对操作、跨线程可见性等。这些语义的底层是通过读写屏障实现的。
若多个 CPU 对相同的数据连续写入,每次读写都会将数据发布到总线上,以维持最新的缓存。
MOESI 协议增加了一个 Own 状态,该状态是脏的且共享的。CPU 之间可以直接共享 Own 状态的缓存行,再进行写入操作,无需写入内存。
在 Shared 状态下,若遇到 BusRd 等事件,可能每个持有缓存的 CPU 都会同时发出 FlushOpt 事件,造成冗余。
MESIF 协议增加了一个 Forward 状态,等价于 Shared 状态,且指定只有 Forward 状态的 CPU 缓存行才能发出 FlushOpt,避免冗余发送。
只要有 Shared 状态的缓存,则必然有且只有一个 Forward 状态。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。