共享内存并行编程(三)为什么需要内存屏障?

Premature abstraction is the root of all evil.

为什么需要内存屏障

答:因为对内存引用操作的重排序能够显著提高性能,所以需要内存屏障。

然而有些情况下必须保证对内存的有序访问。具体而言,考虑同步原语 - 锁的实现,如果获取锁和释放锁不能保证顺序访问,则会出现问题。因此,内存屏障是一个necessary evil。

为了更深入地理解这个问题,需要理解cache是如何工作的,以及那些使得cache能高效工作的微系统架构的实现细节。尤其是下面两个硬件组件

  • store buffer
  • invalidate queue

cache结构

三大miss

  • cold miss
  • capacity miss
  • compulsory miss

多核架构下多了两种miss的情况

  • write miss: CPU对内存地址的写操作需要首先invalidate其它核中cache的副本,之后才能写入。若当前数据在cache中,但被标记为read only则会导致write miss。
  • communication miss: 在发生write miss的invalidate过程后,当其他核希望再次访问该数据,则会导致communication miss。这种情况多发生在多个CPU使用该数据项互相通信的场景。例如使用互斥算法的多个CPU通过”锁”相互通信。

cache一致性协议: MESI协议

状态 是否独享 和内存是否一致 dirty 读写权限 抢占情况
modified CPU有所有权,单份up-to-date拷贝 不一致 dirty r/w 必须写回内存或者传递所有权给其它CPU
exclusive CPU有所有权,单份up-to-date拷贝 一致 非dirty r/w 可以直接抛弃
shared 非独享,多份up-to-date拷贝 一致 非dirty r 可以直接抛弃
invalid
空cache line
       
  • 只有modified状态下cache和内存中数据不一种,数据需要首先写回/放入其它cache才能存放新数据
  • 当其它cpu要读取某个仅在当前cpu上且为modified状态的数据时。cache line状态需要从modified变化为shared,最简单的保证cache和内存一致的方式是写回内存,而一些更实际的协议会增加更多的状态,以避免冗余的写内存操作。

通信消息

若所有CPU使用共享总线,则需要MESI协议需要下面这些消息

消息类型 解释 来源
read 待读的cache line的物理地址 -
read response read消息请求的数据 memory或其它cache
invalidate 待invalidate的物理地址,其它cache必须将该行丢弃,并且每个CPU都需要返回ack -
invalidate acknowledge 丢弃对应cache line之后发出的ack -
read invalidate 将被读的cache line,同时其它cache还需要invalidate -
writeback 将被写入内存/其它cache的cache line的地址和数据,强占modified状态的cache line的位置时使用 -
  • 注意到有趣的是,单个SMP机器内部本身也是一个共享内存的多处理器系统。
  • 注意: 总线同一时间只有一个owner,因此对于并发的invalida消息,最终只会有一个CPU赢得总线的控制权。另一个CPU的cache被invalidate。
  • 注意: 大规模SMP系统中会出现ack消息泛滥的情况,此时可以使用directory based方法。
  • SMP集群 VS SMP系统
    • cache一致性协议较为简单,硬件性能优于软件消息传递
    • 集群性能 > SMP系统
    • SMP编程模型比分布式系统编程模型简单

MESI状态转换

store buffer

invalidate queue