最近实在太忙了,没有时间写技术总结文章。但总结这个习惯不能丢掉。“处理器中的存储问题”专题还有很多topic可说,今天我们继续。
说到存储模型,大家最熟悉的还是顺序一致性模型(Sequential Consistency)和TSO存储模型性(Total Store Ordering)。顺序模型是理想的一种模型,也是最符合直觉的模型。TSO模型对顺序模型稍加改变,对存储器写后跟一个存储器读(不同地址),允许别的硬件线程先观察到读操作,再观察到写操作。为什么会有这样的规定?原因来自于某些处理器实现的时候为了性能的考虑,加了一个store buffer(这里我们用的store buffer都是FIFO性质的),先执行的存储器写的数据可能还存在当前的硬件线程里,后面的存储器读就直接去访问共享存储器了。对于别的硬件线程来讲,当然先观察到了这个读操作,毕竟写操作数据还没flush到共享存储器呢。
看来architecture(存储模型的定义)还会反过来受到microarchitecture(共享存储及write buffer,总线等等)的约束。就比如,x86的存储模型一开始并不是精确定义的,而是使用一些模棱两可的通俗说法加上一些例子来说明,看起来就像是先做好了实现,再根据实现来描述对它的定义,颇有一些先射箭后画靶的感觉。之后的各路学者为x86定义了各种细微区别的模型,总结出了x86处理器的各种特性(甚至是找到其bug——和spec不相符的行为)。随着多处理器的发展,如今的存储模型的定义已经不满足于模棱两可的文字说法,改为定理形式的描述,比如RISC-V。
既然write buffer的存在及其提高performance的意图使得对不同地址的写后读可能在其他处理器眼里被reorder了,那么对相同地址呢?考虑到写和读该地址可能在不同的处理器(硬件线程)上,保险的做法就是先write back再读,IBM370模型就是这样做的。如果允许从自己处理器的write buffer里读,那么这就是TSO模型,如果还允许从别的处理器的write buffer里读,那么就是PC模型。PC利用了某些互联网络传输的特性:不同节点直接的latency可能不同,这样一来PC模型中的不同处理器的写在不同处理器看来顺序可能不同。TSO模型是根据SPARC创建的,当然SPARC后来也用PSO,而x86实现则认为和TSO模型差不多,后来有学者命名为x86-TSO。PC模型VAX用过,好怀旧的名词。
programmer会怀念SC的美好,在SC下不用头痛这些形形色色的由具体实现、各种妥协引起的烧脑问题。这里还仅仅是放松了写到读的顺序,还有更多relax的其他模型。SC是programmers, implementers的乌托邦,在program order和atomicity(原子性)两方面都是最符合人类直觉的,最容易理解的。上面三种广义TSO模型中放松写到读的顺序违反了SC的program order要求,PC模型中不同处理器的写被不同处理器看到不同顺序违反了SC的atomicity要求。
我们还是回到现实吧,多处理器系统怎么避免放松program order和atomicity导致的consistency问题?这里我们不关注cache coherence,它是个基础,有cache的系统没有cache coherence就没法保证程序执行正确性了。没有cache的系统同样会有consistency的问题,比如我们今天讨论的write buffer带来的问题。要解决consistency问题,这时候就需要安全网(safety net)出场了。由于不同的处理器模型绑定到对应的实现上(先射箭后画靶),所以安全网的实现也不同,比如IBM370模型使用序列化(serialization)指令,TSO和PC模型使用RMW指令。
具体在x86系统里各种LOCK指令对应于RMW指令,解决了atomicity的问题;MFENCE指令解决了program order的问题,有这些指令的帮助,可以在关键代码上实现SC。顺便提一下x86里面的三条fence指令MFENCE, LFENCE和SFENCE,字面上看像是解决consistency的问题,但实际上MFENCE确实是一条非常强的barrier指令,可以解决TSO模型所需的reorder需求,而LFENCE和SFENCE解决是单处理器情况下的一些特殊问题,知乎上有个主题讨论了这个问题(https://www.zhihu.com/question/29465982)。 由此看来,相当多的指令是为了解决实现中,或者说是microarchitecture,中的一些具体问题的,从这个角度看RISC-V指令集的确纯粹一些,但也无法彻底免俗。