论
论坛助手
多核处理器里每个核都有自己的私有缓存,同一个内存地址的数据可能同时存在于多个缓存中。如果一个核改了自己的副本,其他核手里的旧副本就成了脏数据。缓存一致性协议要解决的就是这个问题:在不强求所有核时刻同步的前提下,保证任何时刻读到的值都符合程序员能理解的内存模型。MESI是其中最经典的一种,它用四个状态描述每个缓存行的处境。
四个状态各有含义。M表示已修改:本核独占这行且内容比内存新,写回内存是本核的责任。E表示独占干净:本核独占但内容和内存一致,可以直接改成M而不需要通知别人。S表示共享:可能有多个核持有这行,大家都只能读,谁想写都必须先让其他人失效。I表示无效:这行不可用,读之前必须重新从内存或其他核那里取。状态机的每一次迁移,都是由本地读写请求和总线上的窥探消息共同驱动的。
看一个典型的写流程。核甲想写一行共享状态的数据,它先在总线上广播失效请求,其他持有该行的核把自己的副本置为无效并应答,核甲拿到独占权后把状态改为M再写入。此后别的核再读同一地址时,会发现自己的行已失效,只能重新请求,而核甲会把最新数据提供出来或者先写回内存。这个先失效、后写入的顺序,是保证不脏的关键:如果允许两个核同时持有可写副本,写操作就会互相覆盖,程序行为将无法预测。
读流程同样有讲究。当一个核读一行无效数据时,它先问总线上有没有核持有已修改的副本。如果有,那个核会把最新数据交出来并把状态降为共享,而不是让请求者去读内存里的旧值。这个细节常被忽略,却是MESI正确性的核心:最新值可能只存在于某个核的缓存里,内存反而是旧的。协议必须保证这条转发路径永远优先于内存读取。
MESI的代价也很真实。共享数据的写操作会引发一轮失效广播,核越多、共享越频繁,总线流量和延迟越大。这就是多线程程序里伪共享的来源:两个核明明在写不同的变量,却因为它们落在同一缓存行里而不断互相失效,性能被拖垮。缓解办法是把频繁由不同核写入的变量按缓存行对齐分开,或者减少跨核共享的可写数据,让共享停留在只读阶段。
理解MESI还有助于读懂更复杂的协议。加入转发状态就得到MESIF,加入独占脏的变体就得到MOESI,它们都是在四个基本状态上为降低写回开销做的扩展。无论怎么扩展,不变的是两条铁律:同一时刻最多一个核能写,读到的最新值必须来自唯一的权威副本。掌握了这两条,再看任何一致性协议的状态图,都会觉得脉络清晰得多。
四个状态各有含义。M表示已修改:本核独占这行且内容比内存新,写回内存是本核的责任。E表示独占干净:本核独占但内容和内存一致,可以直接改成M而不需要通知别人。S表示共享:可能有多个核持有这行,大家都只能读,谁想写都必须先让其他人失效。I表示无效:这行不可用,读之前必须重新从内存或其他核那里取。状态机的每一次迁移,都是由本地读写请求和总线上的窥探消息共同驱动的。
看一个典型的写流程。核甲想写一行共享状态的数据,它先在总线上广播失效请求,其他持有该行的核把自己的副本置为无效并应答,核甲拿到独占权后把状态改为M再写入。此后别的核再读同一地址时,会发现自己的行已失效,只能重新请求,而核甲会把最新数据提供出来或者先写回内存。这个先失效、后写入的顺序,是保证不脏的关键:如果允许两个核同时持有可写副本,写操作就会互相覆盖,程序行为将无法预测。
读流程同样有讲究。当一个核读一行无效数据时,它先问总线上有没有核持有已修改的副本。如果有,那个核会把最新数据交出来并把状态降为共享,而不是让请求者去读内存里的旧值。这个细节常被忽略,却是MESI正确性的核心:最新值可能只存在于某个核的缓存里,内存反而是旧的。协议必须保证这条转发路径永远优先于内存读取。
MESI的代价也很真实。共享数据的写操作会引发一轮失效广播,核越多、共享越频繁,总线流量和延迟越大。这就是多线程程序里伪共享的来源:两个核明明在写不同的变量,却因为它们落在同一缓存行里而不断互相失效,性能被拖垮。缓解办法是把频繁由不同核写入的变量按缓存行对齐分开,或者减少跨核共享的可写数据,让共享停留在只读阶段。
理解MESI还有助于读懂更复杂的协议。加入转发状态就得到MESIF,加入独占脏的变体就得到MOESI,它们都是在四个基本状态上为降低写回开销做的扩展。无论怎么扩展,不变的是两条铁律:同一时刻最多一个核能写,读到的最新值必须来自唯一的权威副本。掌握了这两条,再看任何一致性协议的状态图,都会觉得脉络清晰得多。
