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有相同数据的缓存;写缓存时需要:
    1. 先向其它core发送缓存失效指令
    2. 等待其它core对缓存失效指令的确认回复【其它core收到后,无效化相关的缓存块,之后发送确认回复】
    3. 收到所有确认回复之后,才能对缓存进行更新,并切换状态到M

I

  • 无效状态,得先将数据从主存加载到缓存,才能进行后续的读写

伪共享问题

两个无关的变量A,B,由于处在同一个cache line上,且MESI的最小作用单位为一个cache line,因此如果两个核心上的两个线程分别读写自己的变量,则会导致不断的缓存失效:这就是伪共享问题

避免伪共享问题

  1. 经常会修改的热点数据,要避免刚好在同一个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 bufferinvalid queue产生效果
    • 例如arm64的DMB指令
    • 做的事情太多,性能下降

读写屏障

  • alpha架构
写屏障:精细控制store buffer 【StoreStore barrier】

写屏障的作用是让屏障前后的写操作都不能翻过屏障。也就是说,写屏障之前的写操作一定会比之后的写操作先写到缓存中。写屏障时,暂停处理所有已发出的缓存写;处理完后,才会越过屏障

读屏障 :精细控制 invalid queue 【LoadLoad barrier】

保证屏障前后的读操作都不能翻过屏障。假如屏障的前后都有缓存失效的信息,那屏障之前的失效信息一定会优先处理,也就意味着变量的新值一定会被优先更新。读屏障时,暂停处理所有已经收到的缓存失效请求;处理完后才会越过屏障

单向屏障: half - way barrier

  • arm
    它并不是以读写来区分的,而是 像单行道一样,只允许单向通行,例如Arm中的stlrldar指令就是这样

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来代替。


MESI:为什么还需要内存屏障
http://example.com/2024/09/20/嵌入式-架构/MESI - 为什么还需要内存屏障/
作者
Cyokeo
发布于
2024年9月20日
许可协议