1、前言
现在大多数多核芯片在硬件中支持共享内存,设计和评估一个正确的共享内存系统需要准确理解内存模型。不同CPU可能采用不同的内存模型,比如ARM和RISC-V的Related模型,Intel和AMD的TSO模型以及IBM的Power模型等等。尽管这些模型千奇百怪,各有优缺点,但我们只要抓住它们的本质,就可以轻松拿捏它们。不要太在意边边角角的点,不然很容易陷进去。本文闲聊的内容框架大致如下,如果有出现专有名词,就不再解释了,请各位自行搜索下,或者翻看我的历史文章。
2、内存模型
Memory consistency model又称Memory model (内存模型),定义了使用Shared memory(共享内存)执行多线程(Multithread)程序所允许的行为规范。
内存模型主要是抓住program order(程序顺序)和global memory order(全局内存顺序),根据这两个order允许的行为,可以把内存模型划分为SC(Sequential Consistency)、TSO(Total Store Order)和Relaxed(weaker) model。
简单来说,如果global memory order保持每个Core的program order,那么内存模型就是SC模型。SC模型是最直观而且最容易让程序员推理代码的执行结果,但过于死板,限制了硬件的发挥,于是就进一步发展了TSO模型。
不管地址是否相同,TSO模型的older store和younger load的program order在global memory order中可以不保留,这就允许Core中加入FIFO类型的Store buffer,用于缓存已经在Core pipeline上Commit的Store数据,但还没有写到memory当中,隐藏了store写到memory的latency来提高性能,后续的younger load如果发现可以从Store buffer中获取想要的数据,那么Store buffer可以直接forward过去。在TSO模型中,对于单个Core来说,程序的执行结果看起来还是符合预期。但多个Core情况下,会有一些出乎意料的结果,因此引入了FENCE指令,如果想要让older store和younger load的program order一定与global memory order一致,那么需要在它们两个之间插入FENCE指令。TSO模型一定程序上也限制了硬件的优化,于是就进一步发展了Relaxed模型。
Relaxed模型只寻求保留程序员需要的顺序,让一些程序员不关心的指令顺序可以乱序执行,从而允许软硬件更多的优化。Relaxed模型对于不同地址的load和store,可以让它们乱序执行,对于同地址的load和store,让它们保持TSO模型的规则,也就是older store和younger load可以不遵循global memory order,但load必须可以得到最新store的值。这就允许Core可以将TSO中FIFO类型的Store buffer升级为非FIFO类型的Store buffer。且允许多笔store数据的merge行为。在Relaxed模型中,对于单个Core来说,程序的执行结果看起来还是符合预期。但多个Core情况下,会出现更多出乎意料的结果,因此也引入了FENCE指令,以便程序员可以指示哪些指令间需要遵循global memory order。
总得来说,这三类模型,对于单Core来说,程序执行结果是确定的,但对于多Core来说,程序执行结果会存在不一致的情况。SC模型为同一线程中load和store的所有四种组合(Load->Load, Load->Store, Store->Store, Store->Load)提供global memory order保序。TSO模型为同一线程中load和store的三种组合(Load->Load, Load->Store, Store->Store)提供global memory order保序,不对 Store->Load做global memory order保序。Relaxed模型对不同地址的memory操作,允许它们乱序执行,但是对相同地址的memory操作,它和TSO的规则一致。
3、一致性协议
虽然一致性协议众多,但本质上讲,所有一致性协议都通过将写入的数据传播到所有一致性cache来使一个core的写入对其它core可见。对于一致性协议,需要谨遵两个原则:
原则一:Single-Writer, Multiple-Read(简称SWMR),也就是在任意时刻,对于某一个地址,只有一个Core可以写它(也可以读它),或者多个Core读它。永远不会存在一个Core可以写入地址A,而其它Core可以同时读取或写入地址A的情况。
原则二:Data Value Continuity(简称DVC),对于一个地址所存储的数据,它的值是连续的,所有Cores要以相同的顺序看到该地址的写数据。具体来说,可以把时间切割成很多小片段,上一个片段结束的数据值与下一个片段开始的数据值相同,而且处于同一个切片中,所有Core能读取到的数据必须相同,不可能存在在某个片段中,Core0对地址A读取到x,Core1对地址A读取到y的情况。也就是每个地址的数据值需要被正确传播。
4、挂死
挂死(hang)顾名思义就是Core无法正常推进了,要么Core的PC值不再变化,要么Core的PC值一直处于某个循环之中,无法进行其它操作。主要三类死锁、饿死和活锁。
死锁:如果有两个或多个参与者互相等待对方执行某些操作,那么就可能产生资源循环依赖,导致死锁。特别是如果有个多个请求者在共享某些资源,而且它们的动作之间还存在依赖,就要警惕死锁。
饿死:如果一个Core或多个Core无法取得进展,而其它Core仍然可以取得进展,没有进展的Core就处于饿死状态。常规例子就是不公平仲裁容易出现饿死。因此,当有多个请求者争抢某个资源时,一定要注意是否有可能某个请求者永远也无法获取到该资源。
活锁:如果有两个或多个参与者执行操作并改变状态,但它们最终从未取得进展的情况,就发生活锁了。看似大家都在认真干活,但是什么都没有干成。常规例子就是多个Core使用exclusive操作进行抢锁导致的活锁。因此,如果某个资源会被多个请求者共享,请求者抢到后还必须进行某些操作才会释放,否则就一直争抢,有这种倾向的场景就必须要注意活锁的存在。