MESI:为什么还需要内存屏障
参考文档
使用总结 - 多核多线程开发
- 屏障前的读写,发生在之后的读写之前
- 这里的发生可以理解为
- 写:已经写到了其它core的缓存中/主存中,即该更新值对系统内的其它master是可见的
- 读:已经读到了最新值
- 这里的发生可以理解为
- 从某个core上观察
- 自己在执行某条指令时
- 另外的CPU可能在执行任意一条待执行指令
- 因此在某些关键阶段,需要对多核并发进行精细的控制
MESI 简要回顾
单核场景下,通过
- 写通(write through)
- 写回(write back) : 只有当脏缓存块被换出时才写入主存
可以保证缓存与主存的一致性。然而在多核场景下,事情变得复杂了起来
core 0的写还没有更新到主存,core1请求读,将读到旧值;或者core1读到其自己缓存中的旧值
因此需要更多的方法来保证多核间缓存一致性,同步多核缓存中的数据
- 写传播:core0的缓存更新后要通知给其它核,并将缓存更新传播到其它核的缓存中【如果此时其它核上也有这个数据的缓存时,没有的话就不用了】
- 多核同时写相同数据的缓存的完成顺序,在多个核看起来顺序需要是一致的。叫做事务的串行化
要实现事务的串行化,要做到两点
- core对缓存的操作要同步给其它core
- 要引入锁🔒:如果两个core有相同数据的缓存,那么对于这个缓存的更新,只有拿到了🔒才能进行
总线嗅探
core将监听总线事件,当出现缓存更新事件时,core会比较自己有没有相同数据的缓存,如果有的话,就更新自己的缓存内容
MESI状态更新简要
M
- 当前处于修改模式,再写时,无需写传播
E
- 当前处于独享模式,写缓存时,无需写传播
S
- 当前处于共享模式,其它core有相同数据的缓存;写缓存时需要:
- 先向其它core发送缓存失效指令
- 等待其它core对缓存失效指令的确认回复【其它core收到后,无效化相关的缓存块,之后发送确认回复】
- 收到所有确认回复之后,才能对缓存进行更新,并切换状态到M
I
- 无效状态,得先将数据从主存加载到缓存,才能进行后续的读写
伪共享问题
两个无关的变量A,B,由于处在同一个cache line上,且MESI的最小作用单位为一个cache line,因此如果两个核心上的两个线程分别读写自己的变量,则会导致不断的缓存失效:这就是伪共享问题
避免伪共享问题
- 经常会修改的热点数据,要避免刚好在同一个cache line上;即单独的共享数据要与缓存行大小对齐:用空间换时间
有了MESI为什么还需要内存屏障
问题根源 - 写
可以看到在S状态时,想要修改缓存,有可能需要等待很长时间
解决方案
- 引入store buff;==写缓存时将数据写入store buff==
- 并由store去执行漫长的核间MESI同步
- 为提升性能,放弃了缓存一致性【弱缓存一致性】
- 但是又引入了新的问题:store buffe的运行机制会导致 -> 写到缓存/主存的先后顺序可能与程序序不一样
- CPU乱序执行
- 【即使CPU不乱序执行】后写的缓存行可能处于E/M状态,而先写的缓存行可能处于S状态;因此后发出的缓存写的可能先于先发出的缓存写的到达主存 -> 乱序完成
这个问题就交给工程师吧
- 需要工程师自己在合适的地方加内存屏障【硬件无法为我们提供进一步的保证了】
- 暂停让store buffer完成所有缓存更新的同步
- 保证先发出的缓存写先走完MESI的整个流程;再走后发出的缓存写的MESI
- 保证其它CPU能观察到CPU0按顺序的缓存更新
- 使用写相关内存屏障的时候,要考虑到stroe buffer机制
问题根源 - 读
MESI中写时,要向其它CPU发缓存失效请求,并等待回复;其它CPU在接收到请求后,从把缓存置为无效,到发出确认回复的过程是比较漫长的
- 容易导致store buffer写到cache的等待时间较长,导致store buffer容量爆掉
解决方案
- 引入 invalid queue 失效队列:
- 收到 invalid 缓存块的消息后,立即向对方回复✅
- 但并没有把自己的缓存由S更新为I
- 而是把这个失效消息放入queue中
- 等到空闲的时候再去处理失效消息。这里的空闲,在编程开发时要认为是未处理
- 引入了新的问题:
- core1收到core0的无效缓存块消息后,放到了queue中;然而从放入,到该invalid实际被core处理还要经过一段时间;如果在这段时间内,core要读取对应的数据,则仍然使用自己缓存中的旧值
也交给工程师处理
- 在作出这种关键的读取之前,要加入内存屏障
- 暂停后面的读写指令,等待core处理完invalid queue中的无效消息
内存屏障分类
强屏障 【全量屏障】
- 有些内存屏障可以同时对store buffer、invalid queue产生效果
- 例如arm64的
DMB指令 - 做的事情太多,性能下降
- 例如arm64的
读写屏障
- alpha架构
写屏障:精细控制store buffer 【StoreStore barrier】
写屏障的作用是让屏障前后的写操作都不能翻过屏障。也就是说,写屏障之前的写操作一定会比之后的写操作先写到缓存中。写屏障时,暂停处理所有已发出的缓存写;处理完后,才会越过屏障
读屏障 :精细控制 invalid queue 【LoadLoad barrier】
保证屏障前后的读操作都不能翻过屏障。假如屏障的前后都有缓存失效的信息,那屏障之前的失效信息一定会优先处理,也就意味着变量的新值一定会被优先更新。读屏障时,暂停处理所有已经收到的缓存失效请求;处理完后才会越过屏障
单向屏障: half - way barrier
- arm
它并不是以读写来区分的,而是 像单行道一样,只允许单向通行,例如Arm中的stlr和ldar指令就是这样
stlr: store release register -> release 语义
如果我们采用了带有release语义的写内存指令,那么这个屏障之前的所有读写都不能发生在这次写操作之后,相当于在这次写操作之前施加了一个内存屏障。但它并不能保证屏障之后的读写操作不会前移。简单说,它的特点是 挡前不挡后。
- 可以认为屏障之前的写都已经写到了其他core的缓存/主存、或者是使其他core的相关缓存块失效
- 可以认为屏障之前的读都已经处理完了invalid queue,且读到了最新值
需要注意的是,stlr指令除了具有StoreStore的功能,它同时还有LoadStore的功能。LoadStore barrier可以解决的问题是真实场景中比较少见的,所以在这里我们就先不关心它了。对于最常用的StoreStore的问题,我们在Arm中经常使用stlr这条带有release语义的写指令来解决,尽管它的能力相比我们的诉求还是大了一些
ldar: load acquire register -> acquire 语义
作用是这个屏障之后的所有读写都不能发生在barrier之前,但它不管这个屏障之前的读写操作。简单说就是 挡后不挡前。
与stlr相对称的是,它同时具备LoadLoad barrier的能力和StoreLoad barrier的能力。在实际场景中,我们使用最多的还是LoadLoad barrier,此时我们会使用ldar来代替。