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 |
- 只有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编程模型比分布式系统编程模型简单