仓库源文

.. Kenneth Lee 版权所有 2019-2020

:Authors: Kenneth Lee :Version: 1.0

RISCV WMO和TSO具体解决什么问题


本文给人解释RISCV的WMO和TSO模型具体解决什么问题。

多核CPU的Cache问题很麻烦。多核不复杂,就是有多个执行体,各有各的动作而已。Cache 也不复杂,就是访问内存的时候留一份下来,下次访问的时候直接从Cache(通常是SRAM之 类更快的内存)中取而已。但把这两个简单的概念都放进来,这个问题就复杂了:在你的 Cache中的数据,怎么能够让其他人知道呢?其他人按什么顺序知道呢?

定义指令的时候这是个非常复杂的问题,规矩复杂了,代码没法写。规矩简单了,硬件做 不出效率来。为此,我们先定义两个基本的概念(这是本文定义的概念,RISCV不这么叫) :

Memory Order:就是主存看到的数据读写的顺序,简称MO。请注意了,这不一定是物理主 存上看到的顺序,而是所有的执行硬件(RISCV叫HART,ARM叫PE,x86叫Core和SMT),看 到的主存的顺序,你可以在最后一级Cache上模拟出合适的观感来的)

Program Order:就是程序发出读写命令的顺序,简称PO。也请注意了,这不一定是你写的 程序的顺序,因为编译器可以重排你的指令的。我们说的是CPU译码的时候拿到的你发出的 指令的顺序,即使你是多发射,你仍在译码器上表现出一个顺序(根据RISCV的定义,一个 Core只有一个译码器,每个HART只能属于一个Core)。

所以Ordering,就是PO发出的那个顺序,和它自己看到的,以及其他人看到的在PO上表现 出来的顺序的关系。

实际应用中,有两个体系,一个是x86一类的,强顺序模型。这种模型的特征在于:PO发出 一个写,写之前的所有内存操作,都会对所有HART生效。PO发出一个读,读必然先于之后 发出的所有内存操作生效。(这是基础,其实还有其他规则,具体平台会有轻微差别,我们 这里忽略)

另一个体系是ARM一类的,弱顺序模型。这种模型的特征是:只要没有特别声明,没有内存 依赖(比如对同一个地址进行读写),统统不保序。

说起来,前者编程简单,后者硬件性能高。你说谁会赢呢,谁都说不清楚。我们推广ARM的 时候,某个客户说他们的程序在我们上面不能跑了,要我们解决,我看看他的代码——直接 用了一个变量当锁用:我说你要加个Memory Barrier啊。他说:加MB性能不高啊。我说, 那也比你原来在x86上性能高啊。结果人家说:我不管,反正我不用,你给我解决问题(估 计这鸟已经拿原来的性能去找领导报功去了,打死要把锅推给我)。你说,这种情况,你 找谁说理去?

所以,这些模型的问题,就不是个技术问题。所以RISCV就选了个骑墙的做法(其实我觉得 这个恰恰是RISCV的弱点,太骑墙就没有力量了),这鸟两个模型都支持:TSO支持强顺序 ,WMO支持弱顺序。WMO兼容TSO,你的程序支持WMO,可以在TSO上运行,如果你的程序写成 TSO,就需要使用TSO的硬件(或者把系统配置为TSO模式)。

RISCV的WMO模式和ARM不太一样,感觉特“学术化”——这个我喜欢,因为“学术化”一般意味着 特别好理解(好不好用就另说了)。

RISCV给内存访问指令放了两个位和4个类别。位是aq(aquire)和rl(release),aq指令表示 PO上,在它之后的所有同类内存操作都发生在aq之后。rl表示在PO上,所有在它之前发生 的同类内存操作都发生在rl之前。qa和rl可以同时生效。

类别就比较简单了,就是r,w, i, o(内存读写,IO读写)。

如果要跨类别就用fence指令,fence的写法是这样的:fence rwio, rwio。前一个参数分 类别设置aq位,后一个参数分类别设置rl位。这样就可以做到跨类别做memory barrier了 。

暂时来说,我挺喜欢这个设计,但成熟度嘛,我估计还得等两年才敢说。(说起来,看看 这个例子,就知道指令集有多难了。知乎上一大堆说指令集很容易的,真是大言不惭)

补充:关于fence.i

说到fence,我们扩展讨论一下fence.i。fence.i是用来同步数据和代码的。比如你修改了 text段(代码段),代码cache什么时候开始刷新的新的数据呢?这个哪个fence的语义都 解决不了,所以就引入fence.i。fence.i控制了fence.i之前的数据访问被后面的取指感知 到。但很明显,这个定义只能对本Hart生效。如果刷到其他Hart上,一个没有必要(因为 其他Hart也不见得执行这段代码),另一个是同步的语义可能需要再进行精细定义。

当前RISCV在Linux内核(5.0)中对于icache的刷新设计是这样的(内核部分是关抢占的上 下文):

增加一个系统调用: arch/riscv/kernel/sys_riscv.c:SYSCALL_DEFINE3(riscv_flush_icache),用于向内核请 求刷Cache在本地调fence.i完成本地的同步调用SBI(SBI_REMOTE_FENCE_I),要求对所有本 进程涉及的HART发fence.i请求在SBI的处理中(这个代码在固件 machine/mtrap.c:mcall_trap()中),对所有请求的HART发IPI(IPI_FENCE_I)在IPI的处理 中(这个代码在固件machine/mentry.S:trapVector()中),调用fence.i。

这个看着成本就让人发毛,几乎就是无条件广播请求。这个优化很容易,直接让fence.i带 广播能力就可以解决了。问题是,要让性能最优,可能需要带如下参数才合理:

CPU掩码,这个掩码可长可短,支持512个HART不过分吧,靠指令编码进去就不够看,带三 个寄存器,每个64bit,这也不够。另加CSR,这个成本就很高了,还是不值得。带地址范 围,这玩意儿至少也要用掉两个寄存器,也要命。

这样就可以理解为什么RISCV现在要弄得这么脏,要不就用ARM的策略:要广播就都广播, 不想那么多,如果有个地址范围做协助,只要icache中没有这段地址,可以放过这个HART 。要不就是连续范围组播,用两个16位空间分别表示组播的开始和结束位置。