原文:https://gitee.com/laokz/OS-kernel-test/blob/master/memorder/riscv.md
RFC:请求批评指正,勿作引用。本文未解释内存一致性基本概念
《The RISC-V Instruction Set Manual, Volume I: User-Level ISA》明确规范了RISC-V的内存一致性模型,称为“RVWMO” (RISC-V Weak Memory Ordering)。RVWMO主要遵循了RC(release consistency)理论模型,用较少的内存访问顺序约束,为硬件实现和性能优化提供了宽松的条件;同时,禁止若干过于复杂费解的乱序情况,方便了软件程序的开发利用。总体上RVWMO是一种弱内存序模型。
为了便于从x86体系结构向RISC-V迁移,规范还明确了一个称为“Ztso”的标准扩展,提供完全兼容x86架构的RVTSO(RISC-V Total Store Ordering)内存模型。本文主要聚焦RVWMO,从软件视角梳理总结了RISC-V内存一致性模型主要内容。
RISC-V是正处于发展阶段的新兴指令集架构,RVWMO也是如此。这里描述的内容源自2019年最后发布的版本。
为了避免歧义,在介绍RVWMO前先说明几个问题:
用hart表示执行线程或执行单元。RISC-V使用hart一词表示一个逻辑CPU, 即一个硬件线程(超线程)。比如一个双核处理器,每核两个硬件线程,则该处理器有四个hart。hart不仅限于通常意义的处理器范畴,设备控制器甚至CPU集成的内存管理单元(MMU)都可以是hart。这些hart之间共享内存构成了内存乱序的源泉。
指令执行用词。指令发生(happen)、执行(perform、execute)、完成(complete)这几个词对硬件来说有显著区别,但对于软件程序员,并不关心硬件如何分解执行指令,反而关心的是一条指令要么执行了,看到了结果,要么没执行。本文为软件视角,因此在使用这些词汇时,除特别说明外均指代同一含义。
忽略内存对齐和大小问题。内存访问顺序约束的有效性依赖内存地址对齐和大小,未对齐的地址、超过机器字的大小,对内存访问顺序有复杂的影响,一般规律是拆分成多个自然对齐的访存指令,但需要根据规范和具体实现专门考察。本文忽略这一问题,假定所有的内存访问都是“自然对齐”、“自然大小”,都是原子性的访问(single-copy atomicity)。
内存模型与具体实现是两回事。看似废话但值得一提。RVWMO是关于内存访问顺序约束的软硬件接口规范,明确了RISC-V合规架构最少要遵守的规则,提供了“最弱”的内存序约束;但是不排斥具体实现采取更加保守的策略,提供“更强”的内存序约束。因此,RVWMO上的乱序问题,包括litmus模拟测试中的乱序结果,在真实机器上并不必然存在!类似实验数据在《A Tutorial Introduction to the ARM and POWER Relaxed Memory Models》第4.7节可查到。明确这一点仅是为了更加深入地理解内存模型,编程实践中应遵循RVWMO模型以保证最大的兼容性。当然如果你的软件专用于特定机器,根据其特性忽视一些乱序问题可能会有效益。
几个符号表示。xxx ->po yyy
表示程序序中指令xxx在yyy之前,xxx ->ppo yyy
表示全局序和程序序中指令xxx都在yyy之前。
目前RVWMO只是规范了常规内存的一致性要求,没有规范I/O内存的行为。RVWMO明确了13条规则、4条公理,为便于记忆和理解,可以归纳总结为以下4句话:
RVWMO通过全局序(global memory order)定义内存访问顺序约束,对于不允许乱序的情况称为“保留程序序”(preserved program order,PPO)。全局序即所有并行(并发)线程在内存系统中形成的最终内存访问顺序,各个线程对这个全局序的观察都是一致的,除了store buffer带来的“写后读”情况。store buffer是hart私有缓存,用于暂时存储要写入内存系统的数据,这里的数据对本hart可见,即写后再读可以读到这个写入的值,但对其它hart不可见,也因此双方可能观察到不一样的访问顺序。RISC-V包容这种情况。
为了形成一致的全局序,一个hart如果看到了另一个hart的写,则必须所有的hart都看到了这个写,否则会出现不一致(实际硬件实现并不一定有这种“同时看到”时间保证)。这个特性称为多拷贝原子性。也因为包容“写后读”情况,有的称这种原子性为other-multi-copy atomicity。
有了多拷贝原子性保证,程序员就无需再担心“因果性”(causality)、“累积性”(cumulativity)等令人头痛的问题。奇怪的是,规范正文中并没有明确说明这一特性,而仅是在附录中对此进行了解释。
语法依赖准确的定义比较复杂且很绕,想追究细节最好是研读原文。简单地讲,语法依赖是指一条指令的源操作数与前面指令(不一定紧邻)的目的操作数是同一个寄存器,前面不出结果后面没法执行,这种逻辑上的限制“自然而然”地约束指令顺序。理解要点,一是看寄存器名而不分析值,这也是叫语法依赖而不叫语义依赖的原因;二是不是所有指令都有目的操作数,因此没有指令会依赖一条store语句(不包括sc指令),因为它没有目的操作数,见规范14.3节;三是x0寄存器不构成任何依赖,因为它的值是固定的、已知的。
显然,语法依赖具有传递性:B依赖A,则A ->ppo B
;C依赖B,则B ->ppo C
;所以必然A ->ppo C
,即A全局序必在C之前。注意,规范也没有明确指明这一点。
示例1(原文序号为Figure A.10,下类同):
(a) ld a1,0(s0)
(b) xor a2,a1,a1
(c) add s1,s1,a2
(d) ld a5,0(s1)
指令(b)要执行必须(a)先读到a1,指令©要执行必须(b)先计算出a2,指令(d)要执行必须©先计算出s1。这个指令序列自上而下地构成了一个依赖链,因此两条不相关的内存读指令(a)和(d),被强制地保证了执行顺序,尽管指令(b)和©看似可优化掉的无用指令。这种“人为”依赖是性能攸关处的一个备选工具。
对于内存访问操作,语法依赖按寄存器用途分为三类并保证有序:
地址依赖。前一条指令的结果是后一条访存指令的操作地址(用法相当于指针)。如:lw t1, (s0); lw t2, (t1)
。
数据依赖。前一条指令的结果是后一条指令的操作数。如:lw t1, (s0); sw t1, (s1)
。
控制依赖。两条指令间存在一个依赖于第一条指令的分支或间接跳转指令。用C语言的if语句比拟:条件语句对后续的所有指令(包括语句块之外的指令)构成控制依赖。但RVWMO仅保证对后续的store指令有序。
示例2(A.12):
(a) lw x1,0(x2)
(b) bne x1,x0,next
(c) next: sw x3,0(x4)
又是一个“人为”的控制依赖(b)确保了不相关访存操作(a)和©有序。
示例3:
代码1 代码2 代码3 代码4
-------------------------------------------------------------
(a) lw t0, (s0) lw t0, (s0) lw, t0, (s0) lw, t0, (s0)
(b) sw t1, (t0) sw t0, (t1) sw, t1, (t0) lw, t1, (t0)
(c) lw t2, (t0) lw t2, (t1) sw, t2, (s1) sw, t2, (s1)
这四段特殊的代码范例都是前两条指令构成语法依赖,第三条指令进行相关的读或不知是否相关的写。RISC-V根据“几乎所有真实CPU流水线执行机构的行为”,将这种范例中(a)和©的关系称为流水线依赖,并明确规定不能乱序。约束代码1和2的出发点是“在写地址或值未知时不能(无法)读这个写”–(b)地址或值未确定时©不能执行,又因为(b)地址或数据依赖(a),因此©在全局序上不能超越(a)。约束代码3和4的出发点是“前面地址未知时不能写”–(b)地址未确定时©不能执行,以防止地址冲突(参见下文“写不超前”),又因为(b)地址依赖(a),因此©在全局序上也不能超越(a)。规范中流水线依赖规则是单独的一类,列在这里仅为方便,尽管它们看起来不是那么“自然”。
“同一地址”允许地址部分交叉重叠,规则约束的仅仅是这重叠部分(Overlapping-Address)。
任何访存指令 ->po store
,store指令在全局序上不会超越前面的指令。这可理解为,如果允许后面的写乱序,则前面指令应读的值就被覆盖了,或前面的写反而覆盖了后面的写。示例4(A.5):
Hart 0 Hart 1
-------------------------------------
li t1, 1 li t2, 2
(a) sw t1,0(s0) (d) lw a0,0(s1)
(b) fence w, w (e) sw t2,0(s1)
(c) sw t1,0(s1) (f) lw a1,0(s1)
(g) xor t3,a1,a1
(h) add s0,s0,t3
(i) lw a2,0(s0)
Outcome: a0=1, a1=2, a2=0(允许)
先看(d)和(e)的关系,它们是对同一地址的访问,因此store指令(e)在全局序上不能超越(d)。后面还会用到这个示例。
上面示例4的结果,说明执行序列是(f)-(i)-(a)-©-(d)-(e),正是因为(e)的存在,使得同一地址的两个读(f)能读到比(d)更新的值,从而允许在内存序上超越(d)。前面说“写不超前”,那(e)怎么先写的呢?先写的怎么反倒在后面呢?如果没写(f)读的值哪来的呢?硬件告诉你:“我先把(e)写到store buffer里,反正别人也看不见,(f)你就接着往下整吧,回头我看看没错再告诉内存记账”。
示例5(A.6):
Hart 0 Hart 1
-------------------------------------
li t1, 1 (d) lw a0,0(s1)
(a) sw t1,0(s0) (e) xor t2,a0,a0
(b) fence w, w (f) add s4,s2,t2
(c) sw t1,0(s1) (g) lw a1,0(s4)
(h) lw a2,0(s2)
(i) xor t3,a2,a2
(j) add s0,s0,t3
(k) lw a3,0(s0)
Outcome: a0=1, a1=v, a2=v, a3=0(允许)
执行序列是(h)-(k)-(a)-©-(d)-(g),(g)和(h)是对同一地址的读且读到的是同一个值(中间再无其它线程向该地址写),因此允许(h)及之后的指令先完成而不违反(g)、(h)之间的CoRR约束。
再次提示,以上三条针对的是同一地址访问情况。
包括原子指令,因此需要专门的指令来强制约束内存访问顺序。
FENCE 用于约束常规内存和/或设备I/O内存的访问顺序。
格式:fence [iorw], [iorw]
逗号前后的iorw分别表示fence指令要约束的前后指令的类型,i表示设备输入,o表示设备输出,r表示内存读,w表示内存写。
对于常规内存规范只推荐了5种组合:
当需要跨越内存种类明确约束访问顺序时,只能使用fence指令。特别地,访问time、cycle、mcycle控制状态寄存器(CSR)时可能需要fence指令,因为CSRs通常为弱内存序的内存映射I/O单元,与常规内存也无必然的顺序约束;在使用时用i表示CSR读,o表示CSR写。
FENCE.TSO 可选指令,fence的变种,相当于fence rw, rw
,除store ->po load
的情况。
原子指令[.aqrl]
标准扩展A提供了原子操作指令,用于构建线程同步操作,同时提供了可选的单向内存序约束标记。
.aq,约束为acquire内存序,后续的不论是读还是写指令都不超前于本指令执行,如:amoswap.w.aq
。.aq不约束前面的指令。注意:只有aq标记而没有rl标记的sc指令是不合适的。
.rl,约束为release内存序,前面的不论是读还是写指令都在本指令前完成,如:sc.rl
。.rl不约束后面的指令,但RISC-V规定如果后面的指令有aq标记,则约束其不能超越rl标记指令,也就是同一hart的acquire-release标记保护的关键区不交叉、不乱序(这种RC模型称为RCsc,相应允许交叉的称为RCpc)。注意:没有aq标记而只有rl标记的lr指令也是不合适的。
.aqrl,约束为顺序一致(Sequential Consistency,SC)内存序,前面的读写指令发生在本指令之前,后面的发生在本指令之后。对于lr/sc原子指令对来说,SC内存序约束应采用lr.aq/sc.aqrl
序列,因为该原子指令执行的标志是成功的sc操作,sc.aqrl确保了前后指令均不越界;反过来如lr.aqrl/sc.rl
,其它hart可能观察到sc后的指令发生在sc之前;当然lr.aqrl/sc.aqrl
是可以的,但一般没必要。
全省略时,没有约束。
程序执行时有很多隐式的内存访问操作,如CPU指令预取单元取指令、内存管理单元(MMU)访问页表等,当这部分内存发生变化时,为了保证变化及时生效,也需要约束内存访问顺序,更准确地说要同步各单元的操作。这部分执行机构使用一些专门的部件,像指令缓存、页表缓存、TLB等,因此需要专门的内存序约束指令。
FENCE.I 由指令集的Zifencei扩展定义,确保指令内存(代码段)的动态更新,对当前hart的指令预取单元可见。
格式:fence.i
在即时编译(JIT)等场景中,执行fence.i
确保本hart所作的动态代码更新,和已传播(propagated)到本hart的其它hart所作的更新,即时生效。但fence.i
不负责这些数据在各hart间的传播,因此要使动态更新对其它hart可见,执行更新的hart应执行前述的fence
指令,并通过核间中断通知其它hart执行fence.i
指令。
SFENCE.VMA 《The RISC-V Instruction Set Manual Volume II: Privileged Architecture》定义的特权指令,同步页表更新。在启用虚拟内存的情况下,系统执行时MMU会隐式地访问页表,缓存有关数据,执行虚拟地址转换,并预读数据,这些操作通常在显式内存访问之前执行。更新页表相关数据时,软件必须确保系统失效这些预先工作,重新使用新属性进行访问。与fence.i
一样,sfence.vma
仅作用于当前hart。
格式:sfence.vma vaddr, asid
vaddr=x0,asid=x0时,作用于所有地址空间的各级页表访问操作
vaddr=x0, asid!=x0时,作用于指定地址空间(address-space identifier)的各级页表访问操作,不包括全局映射
vaddr!=x0, asid=x0时,作用于所有地址空间指定虚拟地址对应的页表项访问操作
vaddr!=x0, asid!=x0时,作用于指定地址空间指定虚拟地址对应的页表项访问操作,不包括全局映射
主要有三种使用场景:
更新PMP寄存器。PMP(Physical Memory Protection)是可选的控制物理内存可访问性的每hart系统寄存器,每个访存操作前都要经过PMP检查。同步指令为sfence.vma x0, x0
。仅发生在M模式。
更新satp寄存器。sapt(Supervisor Address Translation and Protection)是存储进程根页表信息的每hart系统寄存器,类似x86的CR3寄存器。可发生在S或M模式。
更新页表。一般发生在S模式。
RISC-V目前没有规范cache、TLB失效/刷新指令。
规范附录A.5提供了x86、Power、Arm以及linux、C/C++到RVWMO指令的映射指南。
x86/TSO Operation | RVWMO Mapping |
---|---|
Load | l{b|h|w|d}; fence r,rw |
Store | fence rw,w; s{b|h|w|d} |
Atomic RMW | amo loop: lr.{w|d}.aq; |
Fence | fence rw,rw |
Power Operation | RVWMO Mapping |
---|---|
Load | l{b|h|w|d} |
Load-Reserve | lr.{w|d} |
Store | s{b|h|w|d} |
Store-Conditional | sc.{w|d} |
lwsync | fence.tso |
sync | fence rw,rw |
isync | fence.i; fence r,r |
ARM Operation | RVWMO Mapping |
---|---|
Load | l{b|h|w|d} |
Load-Acquire* | fence rw, rw; l{b|h|w|d}; fence r,rw |
Load-Exclusive | lr.{w|d} |
Load-Acquire-Exclusive** | lr.{w|d}.aqrl |
Store | s{b|h|w|d} |
Store-Release | fence rw,w; s{b|h|w|d} |
Store-Exclusive | sc.{w|d} |
Store-Release-Exclusive | sc.{w|d}.rl |
dmb | fence rw,rw |
dmb.ld | fence r,rw |
dmb.st | fence w,w |
isb | fence.i; fence r,r |
* ARMv8也是遵循RCsc模型,不允许.rl ->po .aq
乱序。为了约束这个顺序,这里的映射统一在load前加上强fence(RISC-V不推荐使用fence rw, r的用法)。
** 同上,这里的映射统一在lr指令加上.rl标记,这样sc.rl不会越界,普通的Store-Release也不会越界。
Linux Operation | RVWMO Mapping |
---|---|
smp_mb() | fence rw,rw |
smp_rmb() | fence r,r |
smp_wmb() | fence w,w |
dma_rmb()* | fence r,r |
dma_wmb()* | fence w,w |
mb() | fence iorw,iorw |
rmb() | fence ri,ri |
wmb() | fence wo,wo |
smp_load_acquire() | l{b|h|w|d}; fence r,rw |
smp_store_release() | fence.tso**; s{b|\h|w|d} |
* linux5.8中的定义是fence ri,ri
和fence wo,wo
。这里的映射是假定DMA控制器符合《RISC-V UNIX-Class Platform Specification》的一致性规范,而linux目前尚无这种假定。
** linux中的定义是fence rw,w
。Linux-Kernel Memory Consistency Model(LKMM)和memory-barriers.txt对同一hart上同一变量保护的关键区之间的内存序关系,如unlock ->po lock
、release ->po acquire
情况,描述的比较模糊,litmus模拟测试显示是RCpc关系–即没有顺序约束!这很奇怪!LKMM的维护者称至少对于锁保护的情形,事实上linux支持的架构基本都是TSO约束的–即除了store ->po load
外,两个关键区之间的其它关系对都是遵守程序序的,为此提出修改LKMM反映这一事实。这里的映射就是为了适应这一变化,但目前修改还没有实现。
Linux Construct | RVWMO AMO Mapping |
---|---|
atomic_ |
amo |
atomic_ |
amo |
atomic_ |
amo |
atomic_ |
amo |
Linux Construct | RVWMO LR/SC Mapping |
---|---|
atomic_ |
loop: lr.{w|d}; |
atomic_ |
loop: lr.{w|d}.aq; |
atomic_ |
loop: lr.{w|d}; fence.tso*; loop: lr.{w|d}; |
atomic_ |
loop: lr.{w|d}.aq; |
* 基于前面同样的理由,这里加强了release语句的约束。
C标准的有关表述硬是看不懂:-/,请高手解读!
C/C++ Construct | RVWMO Mapping |
---|---|
Non-atomic load | l{b|h|w|d} |
atomic_load(memory_order_relaxed) | l{b|h|w|d} |
atomic_load(memory_order_acquire) | l{b|h|w|d}; fence r,rw |
atomic_load(memory_order_seq_cst) | fence rw,rw; l{b|h|w|d}; fence r,rw |
Non-atomic store | s{b|h|w|d} |
atomic_store(memory_order_relaxed) | s{b|h|w|d} |
atomic_store(memory_order_release) | fence rw,w; s{b|h|w|d} |
atomic_store(memory_order_seq_cst) | fence rw,w; s{b|h|w|d} |
atomic_thread_fence(memory_order_acquire) | fence r,rw |
atomic_thread_fence(memory_order_release) | fence rw,w |
atomic_thread_fence(memory_order_acq_rel) | fence.tso |
atomic_thread_fence(memory_order_seq_cst) | fence rw,rw |
C/C++ Construct | RVWMO AMO Mapping |
---|---|
atomic_ |
amo |
atomic_ |
amo |
atomic_ |
amo |
atomic_ |
amo |
atomic_ |
amo |
C/C++ Construct | RVWMO LR/SC Mapping |
---|---|
atomic_ |
loop: lr.{w|d}; |
atomic_ |
loop: lr.{w|d}.aq; |
atomic_ |
loop: lr.{w|d}; |
atomic_ |
loop: lr.{w|d}.aq; |
atomic_ |
loop: lr.{w|d}.aqrl; |
程序员–自如运用规则1(同一地址写不超前)和4-8(内存序约束指令)
专家–用规则9-11(语法依赖)加速关键路径
极品–也很少使用规则2-3(同一地址读CoRR、原子不乱)和12-13(流水线依赖)
结语:以上所述是为了能够简单直观地理解把握RISC-V内存一致性模型,实际定义和规则有更为严谨的条件,推荐研读规范以求甚解。需要有所准备的是,硬件实现非常乐意“巧妙”地或说“莫名其妙”地破坏这些严谨的条件,且秘而不宣–CPU微架构实现是商业秘密,因此准确把握和利用这些规则并不容易。为此,建议在使用中,一是不设计逻辑复杂的依赖、同地址约束代码块,二是防止编译器、汇编器优化掉设计的语句,三是利用litmus模拟测试进行验证。
(2020-08-20 ver 1.0)