基本逻辑
各个CPU构架的弱内存模型都会定义一些规则,这些规则约束各种内存序,在这些规则之外
各种内存访问都是可以乱序的,所谓barrier指令只是其中的一些规则,这些规则用指令的
方式显示约束了内存序,所以,只从barrier指令去看内存序,始终是只能看到其中的一部分,
整体认识上还是模模糊糊。如果编程中会遇到可能的内存序问题,这些规则怎么说也的通读
一遍,不然基本上就是一个“能跑就行”的程序。
riscv协议在Chapter 14定义了这些规则(ARMv8/v9协议在2.3章定义了相关规则),在附录里
专门做了这些规则的解释(RVWMO Explanatory Material, Version 0.1),在附录里还有内存
序相关的形式化验证模型(Formal Memory Model Specifications, Version 0.1)。
注意内存序(consistency)和cache一致性(coherence)是两个不同的概念,前者是说的是硬件
故意放松一些指令执行上的循序,依此来提高硬件性能,指令的这种乱序行为是程序员可以
直接感知的,本文讨论的就是内存序的相关问题,而后者指的是,多核系统上,因为多核
共享内存或者cache而引发的数据存储一致性上的问题,硬件需要用一定的cache一致性协议
(比如MESI协议)在硬件层面解决这个问题。
riscv内存序规则
riscv在Chapter 14定义了它的弱内存序,定义的逻辑是这样的,”Memory Model Primitives”
这一节是一些术语的定义,”Syntactic Dependencies”这一节定义的是两个指令关系的名称,
这一节并没有定义内存序的规则,后面定义规则时依赖这一节里定义的指令关系,”Preserved Program Order”
这一节定义的是riscv弱内存序的规则,这一节是这一章的主体,一共定义了13条规则(rule),
随后在”Memory Model Axioms”这一节里定义了三条公理(Axioms),这些约束大部分是符合
我们的直观的认识,但是也有一些和直观的认知是不同的。学习这些规则时,我们要认识到
这些规则之外的情况,这些情况都是可以乱序的,barrier指令正是对有可能出现的乱序情况
的约束。下面逐条看下这些规则:
1 | • Overlapping-Address Orderings |
访存地址有重合的情况,第一条规则是说后面的store不能跑到前面的指令之前执行。
第二条规则就比较绕了,直接翻译是,a和b是load操作,x是a和b要读的数据,a和b之间没有
store修改x(在内存)的值,a和b load的值之前被不同的内存写操作修改。这一条规则进一步的
解释是,两个load读同一个地址上值,新的load不能读到旧load读到的值,但是,还要进一步
考虑两种情况,第一种是a和b之间有store修改x的值,这种情况下a和b不保序,第二种情况
是,a和b读到的值是被不同写内存操作修改的值,这种情况a和b也不保序。
第三条规则是说,后面的load不能超过之前由AMO/SC触发的写操作,直观的理解也是这样。
注意,根据如上的定义,当a是一个普通的store,b是一个load,他们访问相同地址,load
是可以跑到store之前的,不过load到的值是store缓存在store buff里的值,这个就是Intel
TSO内存序上唯一放松的内存序。
再次总结下,load/store的四种可能情况是:1. load-load, 2, load-store, 3, store-store,
4. store-load。规则1说的是2/3两种情况不能乱序,规则2说的是1的情况,规则3只说了
AMO/SC-load的情况,不能乱序,所以普通store-load的情况是可以乱序的,这种情况就是
我们上面提到了TSO内存序上唯一的一个放松约束。
1 | • Explicit Synchronization |
用显示的barrier指令指定内存序的情况,这些去看barrier的定义就好,需要注意的点有:
acquire语意是说acquire后面的读写指令不能排到acquire之前(没有约束acquire之前读写
指令的顺序),逻辑示意如下。release的语意正好相反。
1 | +-------------+ |
riscv上acquire/release的作为AMO或者lr/sc指令的属性一起使用,ARM64上则有STLR/LDAR
指令,这两条指令把acquire/release和普通load/store指令结合到了一起。
如下图所示,如果要确保地址无关的两个store不乱序,可以在其中加一个写fence,但是
这样会一把拦住fence前后的所有store,当把后一个store换成带release的store时(STLR),
store和STLR保序的同时,STLR后面其它的地址无关的store/load也可以提前投机执行。
1 | +-------------+ +-------------+ |
第七条规则中的RCsc指的是Release Consistency with sequentially-consistent synchronization
operation,对应的概念还有RCpc,这两者是acquire/release的两种分类,描述的是,比如
上图中如果“other store/load“里如果有LDAR,那么这个LDAR是否可以和STLR乱序,如果
acquire/release的种类是RCsc,那么不能乱序,如果是RCpc,是可以乱序的。riscv的WMO
中的acquire/release的种类是RCsc。
第八条规则说的a和b是一个pair,是说lr/sc指令组成的pair。
1 | • Syntactic Dependencies |
这几条规则说的是指令之间语意上的依赖,和最开始访存地址重合的依赖比较类似。riscv
上对地址依赖和数据依赖的定义如下:对于两条内存操作指令,如果后一条指令的地址输入
寄存器依赖前一条指令,就存在一个地址依赖,如果后一条指令的数据依赖前一条指令,就
存在一个数据依赖,所以所谓地址依赖和数据依赖都描述的是两条访存指令之间的关系。
需要注意的是规则11,这条规则提到了“控制依赖”,控制依赖的定义是,指令a和指令b,
如果中间还有一条分支或者间接跳转指令(branch and indirect jump)m,如果a和m存在
Syntactic Dependencies,那么b和a之间就存在一个控制依赖,而规则11是说,在a和b之间
存在一个控制依赖时,如果b是个store指令,那么a和b是不能乱序的。
riscv spec里在“Syntactic Dependencies”这一节定义各种依赖关系的定义,但是只是说
当两条指令满足某种情况时,它们之间叫什么关系,并不是满足这种关系后就有序的约束,
riscv spec随后在“Preserved Program Order”这一节借助”Syntactic Dependencies”的定义,
描述内存序的约束,规则9和规则10,直接就说满足地址依赖和数据依赖的两条指令有内存
序上的约束,而规则11是说在满足控制依赖关系时,只对后面是store的情况有序上的约束。
规则11的代码示意如下,左边两个是不会乱序的情况,其中一个是汇编实现,一个是对应的
C语言实现,最右边的是需要加读barrier的场景。
1 | load a0, a1(0) a = read(p1); a = read(p1); <--- 这个read后需要一个读barrier |
这条规则是多么的反直觉,但是既然定义成了规则,它就是软硬件约定的编程接口!
1 | • Pipeline Dependencies |
这两条规则是说,依赖之间是可以前后构成依赖链条的。
下面是三条Axioms的定义。
Load Value Axiom: 每个load指令得到值是global memory order上最近一次store的值,这个
store是global memory order或者是program order上在loadz之前。
Atomicity Axiom: 这个已经体现在lr/sc指令定义里。
Progress Axiom: 貌似是说乱序时提前执行总有一定的限制的。
riscv barrier指令
riscv上barrier有fence和fence.i,相关的指令可以参考这里。
ARM内存序以及barrier指令
相关逻辑可以参考这里。
Linux内核内存序介绍
Linux内核有详细介绍内存序的文档:Documentation/memory-barriers.txt。
参考
- https://www.youtube.com/watch?v=QkbWgCSAEoo