摘自曼彻斯特大学伊恩·沃森的讲座
概述
- 我们已经讨论过在单核上的性能优化
- 局部性
- 向量
- 现在让我们优化一个共享内存程序
- 两种架构:
- 基于总线的共享内存机器(小规模)
- 基于目录的共享内存机器(大规模)
基于总线共享内存的体制
基本情况很简单:
体制
- 总线通常是简单物理连接(导线)
- 总线带宽限制了cpu数量
- 可能是多个内存元素
- 当前,假定每个cpu仅有一级缓存
内存一致性面临的问题
- 假定仅一级缓存与主内存
- 处理器写入它缓存的位置
- 其他缓存可能保存了共享的副本-副本将要过期
- 仅仅更新主内存是不够的
案例
处理器1读取X:从内存中读取X值为24并缓存它
处理器2读取X:从内存中读取X值为24并缓存它
处理器1写入X值32:仅更新了它的本地缓存副本
处理器3读取X:应该读取到什么值呢?
内存与处理器2认为X值为24
处理器1认为X值为32
注意:写入缓存是不够好的
总线嗅探
- 每一个cpu(缓存系统)‘嗅探’(即:不断的观察)它缓存数据地址相关的写动作
- 假定总线结构是全局的,即所有的通信都对所有处理器可见
- 更具扩展性的解决方案是:‘基于目录’的一致性方案
嗅探协议
- 写无效
- cpu想要写一个地址,抢占一个总线周期并发送一个‘写无效’消息
- 所有处理器嗅探到缓存‘写无效’消息后,将他们副本相应的缓存行置为无效
- cpu写入它缓存副本(将定现在它也写入了内存)
- 其他cpu的任何共享读现在都将miss,并重新获取新数据
- 写更新
- 想要写数据的cpu抢占一个总线周期,并在更新自己副本时广播新数据
- 所有嗅探缓存的处理器更新他们的副本
- 注意在这两种方案中,同时写的问题都由总线仲裁解决 - 同一时间仅有一个cpu可以使用总线
更新或无效
- 更新看起来最简单,最明显,最快,但是:
- 对同一个数据(字)多次写入(无中间读)仅需要一个无效消息,但是需要对每个监听消息的处理器更新
- 写入(通常)多字缓存块中的同一块只需要一个invalidate,但需要多次更新。
- 由于空间与时间都具有局部性,上述的场景经常发生
- 在共享内存多处理器中,总线带宽是一种宝贵的资源
- 经验表明,invalidate协议使用的带宽要少得多相对于写更新
- 将只考虑invalidate方案的实现细节
实现问题
- 在这两种方案中,如果知道缓存值未共享(在另一个缓存中复制)可以避免发送任何消息
- invalidate表示认为缓存值更新已写入内存。如果我们使用一个‘copy back’方案,其他处理器可能再次获取到旧值在缓存miss情况下。
- 我们需要一个协议来处理所有的这些问题
MESI协议
- 一个实用的多处理器invalidate协议,它试图最小化实用总线
- 允许实用‘write back’方案-即在‘dirty’缓存行被替换之前主存没有更新
- 常规缓存标签的扩展,即无效标签与‘dirty’标签在普通回写缓存中。
缓存行可能是四种状态之一(2bits)
-
已变更 - 缓存行已经被修改,与主存不一致 - 仅存在当前缓存副本(多处理器‘dirty’)
-
独有的 - 缓存行与主存一致,并且仅存在当前缓存副本
-
共享的 - 与主存一致,但是副本也存在其他缓存中
-
无效的 - 缓存行是无效的(如果在普通缓存中)
-
缓存行状态随着内存访问事件的变化而变化
-
事件可以是
- 由本地处理器动作引起的(即缓存访问)
- 由总线动作引起的 - 作为一次嗅探的结果
-
缓存行只有在地址匹配时才受自己状态的影响
-
操作可以非正式的描述,看作本地处理器的动作
- 读命中
- 读未命中
- 写命中
- 写未命中
-
可以通过状态转换图更正式的描述
MESI本地读命中
- 缓存行状态一定是MES三者之一
- 读命中的值一定是正确的本地值(如果是M状态,它一定被本地修改了)
- 简单的返回值
- 没有状态变更
MESI本地读未命中
-
没有其他缓存副本
- 处理器通过总线请求主存
- 主存的value读至本地缓存,标记状态为E
-
已经存在E状态副本
- 处理器通过总线请求主存
- 嗅探缓存将值放入总线
- 主存访问被抛弃
- 本地处理器缓存值
- 二者的缓存行状态至为S
-
多个缓存已经有了S状态副本
- 处理器通过总线请求主存
- 嗅探缓存之一将值放入总线(总线仲裁)
- 主存访问被抛弃
- 本地处理器缓存值
- 本地副本设置为S状态
- 其他副本保持S状态
-
一个缓存副本状态为M
- 处理器通过总线请求主存
- 嗅探缓存将值放入总线
- 主存访问被抛弃
- 本地处理器缓存值
- 本地副本标记为S
- 原值(M)复制回主存
- 原值状态变更M->S
MESI本地写命中
缓存行必须是MES状态之一
- M
- 缓存行是独有的,并且已经为‘脏’行
- 更新本地缓存值
- 没有状态变更
- E
- 更新本地缓存值
- 状态变更E->M
- S
- 处理器在总线上广播一个invalidate消息
- 拥有副本状态为S的嗅探处理器变更状态S->I
- 本地缓存值更新
- 本地状态变更S->M
MESI本地写未命中
具体操作取决于其他处理器中的副本
- 没有其他副本
- 从主存中读取值至本地缓存(?)
- 更新值
- 本地副本状态变更为M
- 存在其他副本,存在一个副本状态为E,或者多个副本状态为S
- 从主存中读取值至本地缓存,总线事务标记为RWITM(read with intent to modify)
- 嗅探处理器看到该动作则将他们的本地副本置为无效状态I
- 本地副本更新并变更状态为M
另一个副本状态为M
- 处理器发布总线事务标记为RWITM
- 嗅探处理器看到该动作
- 阻塞RWITM请求
- 获取总线控制权
- 将它的副本写回主存
- 设置它的副本状态为无效I
- 原本地处理器再次发布RWITM请求
- 当前是简单的无副本场景
- 从主存读取值至本地缓存
- 本地副本值更新
- 本地副本状态变更为M
把所有动作放在一起
- 上述所有信息可以用一个简洁的状态变更图表达
- 图展示了缓存行在处理器中发生了什么,由于
- 由处理器产生的主存访问(读命中/未命中,写命中/未命中)
- 由其他处理器进行的内存访问会导致由这个snoopy缓存观察到的总线事务(主存读,RWITM,Invalidate)
MESI – 本地启动的访问
MESI – 远端启动的访问
MESI 注意事项
- 有一些少数的变种(写未命中场景的特殊做法)
- 如果缓存行状态为M,那么当缓存线被逐出时,正常的“回写”就完成了
- 多级缓存
- 如果缓存是包含的,那么只有最低级别的缓存需要在总线上窥探
目录方案(Directory Schemes)
- 嗅探方案不能扩展,因为它们依赖与广播
- 基于目录的方案允许扩展
- avoid broadcasts by keeping track of all PEs caching a memory block, and then using point-to-point messages to maintain coherence
- they allow the flexibility to use any scalable point-to-point network
关键问题
- 内存与目录带宽的扩展
- 不能集中主存或目录内存
- 需要一个分布式内存和目录结构
- 目录内存需求不能很好的扩展
- Number of presence bits grows with number of PEs
- Many ways to get around this problem
- limited pointer schemes of many flavors
- 行业标准
- SCI: Scalable Coherent Interface,可扩展的一致性接口