写的还是不够深刻,需要把Processor Core的Pipeline纳入进来
前言
对比着看一下ARM和RISCV两个指令架构下面关于barrier的相关指令,有很多ARM的指令透露出一些成熟的思考。
先罗列一下ARM和RISCV的barrier:
ARM Barrier Instruction | RISCV Barrier Instruction |
DMB和DSB有一些参数 OSHLD OSHST OSH NSHLD NSHST NSH ISHLD ISHST ISH LD ST
|
i:设备内存输入(读取) o: 设备内存输出(写入) r:普通内存读取 w:普通内存写入
amo{op}.{w/d}.{rl/aq/aqrl}
之后从icache load的指令会是新的 |
乍一看去:ARM果然是经过多代之后背负着沉重的历史包袱,仅barrier就需要这么多奇奇怪怪的指令;而RISCV,简洁且强大!真棒。
“自控”和“显像”
分析指令集中barrier的设计,首先要确定barrier的属性,按照面向的Master是执行指令的master本身或是非本身,我给出两个自造的词:“自控”和“显像”,两个属性。自控主要是为了防止CPU的乱序执行引起错误的寄存器配置顺序,进而导致错误的执行结果。而显像则是为了使在某个域内的观察者(其他Master)可以在一个确定的点看到指令的执行结果:如执行完一段内存写入后执行data barrier再通知DMA等硬件做数据搬运。
Data Barrier
按照instruction barrier和data barrier,可以分成两大类,本文主要想把Data Barrier讲透。
data barrier毫无疑问是具有“自控”属性的,但更重要的是“显像”属性,“自控”为了“显像”,即在data barrier指令点,对于load/store的执行进行顺序控制-“自控”(load/store是可以单清),保障让指定域(cache/ddr等)内的观察者在data barrier指令点可以看到域内某个数据的变化(显像)。而这,就需要关注最起码2个data barrier的属性:data barrier的模式,data barrier的作用域。
1. data barrier的模式
将data barrier的语义拆细来说,首先可以分成load/store两个方向,而barrier是控制顺序的,于是就会有load/store的先后顺序,而load本质上属于“自控”,而store则为了“显像”,具体我罗列9种模式:
模式a
a. instructions->load0->barrier_point->load1->instructions
前后读保序,其他不保序
此barrier point保证上下游load不会改变顺序,也就是上游load不会拖到此点后,下游load不会提到此点前。也就意味着如果下游的store或其他指令得到机会的话,是可以提前发射的,相对的,上游的store以及其他指令可以由于某些原因(如发生依赖)拖到此点后发射,CPU Pipeline降低空闲程度,多发射也得到保障。
case:load1一定会发生在load0拿到东西之后,似乎看不到什么用处,我假设一个不合理的硬件设计:32bit寄存器A和32bit寄存器B需要顺序读取,然后合并成一个64bit的数据来操作;而硬件上,两个寄存器被设计成错误的模式,即寄存器B被读取过之后,寄存器A会被自动清除;这时候,就需要load0->寄存器A和load1-寄存器B之间插入此barrier。
(这个barrier模式真的不是很有用。。。CPU上一般对这种barrier直接插成load和store全barrier)
只有自控,没有显像;因为只有load,所以根本不会对外部master产生影响。
模式b
b. instructions->load0->barrier_point->store1->instructions
前读后写保序,其他不保序
此barrier point保证上游load和下游store不会改变顺序,也就是上游load不会拖到此点后,下游store不会提到此点前。也就意味着如果下游的load或其他指令得到机会的话,是可以提前发射的,相对的,上游的store以及其他指令可以由于某些原因(如发生依赖)拖到此点后发射。
case0:再假设一个硬件的设计:硬件设计上需要先读寄存器0获取消息,然后再写寄存器1清硬件状态;如果先写寄存器1清掉硬件状态后,会导致寄存器0读不到本次消息,这里就需要插入此barrier。
case1:两个无cache一致性的CPU通讯同样会遇到需要读取->写出通知的情况,如果向对方CPU的通知需要保证在读之后执行,也是需要此barrier保序的。(不是很优雅的设计)
有自控,有显像,控制自己的load0发生在store1之前,使功能正确。
模式c
c. instructions->store0->barrier_point->store1->instructions
前写后写保序,其他不保序
此barrier point保证上游store和下游store不会改变顺序,也就是上游store不会拖到此点后,下游store不会提到此点前。
case:DDR写完数据再写触发DMA
有自控,有显像;保证数据完全写出。
模式d
d. instructions->store0->barrier_point->load1->instructions
前写后读保序,其他不保序
此barrier point保证上游store和下游load不会改变顺序,也就是上游store不会拖到此点后,下游load不会提到此点前。
case: 对某硬件先写入操作码,再读取数据(似乎比较少见)
有自控,有显像,控制store先发生,使功能正确。
模式e
e. instructions->load0->barrier_point->load1 and store1->instructions
前读后读写保序,其他不保序
此barrier point保证上游load和下游load-store不会改变顺序,也就是上游load不会拖到此点后,下游load和store都不会提到此点前。这其实是将barrier提前解除,也就是说,上游load全执行完之后此barrier point就可以解除,也就是此点之前的load需要赶紧执行完,后续load-store才能得到执行,这实际上在CPU的load和store使用共享通路的情况下是有意义的,即load抢先发射并等待结果返回,在之后不能发射load的cycle可以更多填充store指令,降低CPU流水线的气泡。
case: 可实现spin_lock中读半barrier部分,获取锁之前,不会发生对临界资源的load,但是之前的store可以在后面执行。
有自控,有显像,典型的读半barrier。
模式f
f. instructions->store0->barrier_point->load1 and store1->instructions
前写后读写保序,其他不保序
此barrier point保证上游store和下游load-store不会改变顺序,也就是上游store不会拖到此点后,下游load和store都不会提到此点前。这其实也是将barrier提前解除,也就是说,上游store全执行完之后此barrier point就可以解除,也就是此点之前的store需要赶紧执行完,后续load-store才能得到执行,即store抢先发射并等待完成,在之后不能发射store的cycle可以更多填充load指令,降低流水线的气泡。
先显像,似乎没有意义
模式g
g. instructions->load0 and store0->barrier_point->load1->instructions
前读写后读保序,其他不保序
此barrier point保证上游load-store和下游load不会改变顺序,也就是下游load不会提前到此点前,但是下游store是可以提前的。也就是说,上游load-store全执行完之后此barrier point就可以解除,也就是在上游load-store都需要清空导致的Pipeline气泡中,此点之后的store是可以提前上来填充气泡的,降低流水线的气泡。
case: 暂无
模式h
h. instructions->load0 and store0->barrier_point->store1->instructions
前读写后写保序,其他不保序
此barrier point保证上游load-store和下游store不会改变顺序,也就是下游store不会提前到此点前,但是下游load是可以提前的。也就是说,上游load-store全执行完之后此barrier point就可以解除,也就是在上游load-store都需要清空导致的Pipeline气泡中,此点之后的load是可以提前上来填充气泡的,降低流水线的气泡。
case: 可实现spin_unlock中写半barrier部分
保障解锁之前所有对临界资源的store都完成
之后再修改锁控制value
使其他想要spin_lock的master都能够在临界资源被操作完毕后才看到锁value改变(才可获取锁)
有自控,有显像,典型的写半barrier。
模式i
i. instructions->load0 and store0->load1 and store1->instructions
前读写后读写保序,其他不保序
这是一个全data barrier,前后的读写保序
case:就是DMB,不必说了
有自控,有显像,一整个的data barrier
模式j
j. instructions0->load0 and store0->barrier_point0->instructions1-barrier_point1->load0 and store0->instructions2
纯粹的load aquire和store release,单独使用不能保序,组合使用在load aquire前-store release后的指令之间保序
barrier point0保证下游的load-store不会提前到此点之前,下游barrier point1保证上游load-store不会落到此点之后;这样两个barrier point中间夹住的load-store指令序列instruction1将形成一个临界区域。绕口一点说法是:instrucions0和instructions1这两段指令序列不保证顺序,instrucions1和instructions2这两段指令序列也不保证顺序,但是instrucions0和instructions2这两段指令序列却是保障了顺序的。再进一步解释:虽然instructions0和instructions1这两段指令序列是不保证顺序,但是如果instructions0的指令序列尚未完全返回结果,instructions2之后的指令是不会被执行的。CPU Pipeline能容纳的指令不会是无限的,一定有一个值当第n条指令进入流水线的时候,第n-x条指令完全退出了流水线,而x就是CPU Pipeline能容纳的最大的指令数量。这就鼓励instructions1中的临界区域指令变长,如果足够长就可以将barrier过渡到完全没有开销。
case0(0开销barrier):
假设CPU Pipeline中最多容纳100条指令(第100条指令发射的时候,第1条指令之前的指令一定完全退出了流水线,也就是上文中的x为100),load-aquire和store-release中夹着100条指令。
A-1000 instructions
load-aquire barrier instruction
B-100 instructions
store release barrier instruction
C-1000 instructions
按照上述指令序列,A指令序列在执行到load-aquire的时候,会开始进入A-B混合发射的阶段,按照CPU Pipeline最多容纳100条指令的假设,当C指令开始被发射的时候,A指令序列一定完全退出了流水线;CPU Pipeline中的指令顺利切换到B-C混合发射阶段,因此一定是A指令序列执行完后,C指令序列才开始执行。而在当前case中,load-aquire和store-release这两个"半barrier"仅有解析执行这两条指令开销,没有对CPU流水造成任何影响。
case1(99%完整barrier开销):
同样假设CPU Pipeline中最多容纳100条指令(第100条指令发射的时候,第1条指令之前的指令一定完全退出了流水线,也就是上文中的x为100),load-aquire和store-release中仅有1条指令。
A-1000 instructions
load-aquire barrier instruction
B-1 instructions
store release barrier instruction
C-1000 instructions
与case0相比,case1的B指令序列只有1条指令,A指令序列在执行到load-aquire的时候,会进入A-B混合发射的阶段,但是由于B只有一条指令发射之后即出现store-release,由于A指令序列尚未推出CPU Pipeline,C中指令无法发射,CPU执行C指令序列的时候只允许Pipeline中有B的1条指令,所以Pipeline开始逐渐被清空,或只剩下B,(如果B先执行完的话,CPUPipeline会完全清空)。最终可能发生B-C混合发射,也可能是CPU Pipeline完全清空后再发射C的指令序列(此时相当于一个全data barrier)。所以在当前case中,load-aquire和store-release这两个"半barrier"的开销是接近清空Pipeline的,对CPU流水造成断流的影响。
另外
不过需要提出的是,上述9中模式并不包含非load/store指令的屏障,也就是说,其他非load/store指令如果没有数据依赖的话,是可以任意填充到data barrier前、后发射的。也就是说,实际上加上可以对所有指令都进行屏障的模式的话,会有更多的模式。
小结
RISCV和ARM对barrier的定义还是有不少差别的,仅考虑data barrier来说,ARM支持DMB和DSB区分只data barrier load/store或data barrier所有指令,而RISCV并没有区分仅barrier load/store还是barrier所有指令,按照RISCV的spec理解,是仅barrier load/store,或许RISCV的设计认为编译器和处理器的指令依赖处理会搞定剩下的事情。ARM data barrier模式上只有e/c/h/j三种模式,对应的RISCV中fence [iorw] [iorw]是任意组合的,也就是data barrier的模式是a/b/c/d/e/f/g/h/i,另外,RISCV同样存在模式j的barrier语义,不过不是单独出现,而是融合在atomic指令amo{op}.{w/d}.{rl/aq/aqrl}。
2. data barrier作用域
load、store操作是存在作用域的,比如对Cacheable的内存进行读写,短时间内都只会在cache中发生作用,甚至是只在L1 cache发生作用,需要经过一段时间或是cache flush才会同步到DDR;而data barrier的作用域即表示在data barrier点,load或store需要同步到L1 cache、L2 cache、L3 cache或DDR。
再看ARM的DMB/DSB定义,其支持的data barrier作用域在ARM中称为non-shareabel/inner-shareable/outer-shareable三层,也就是L1 cache/L3 cache和DDR。对应的RISCV中fence [iorw] [iorw]作用域由io和rw区分,io作用域到外设可见,rw作用域为CPU范围(如L3 cache)。
最后
结束
回到最初的barrier设计的初衷,我们假设一个3G Hz主频的CPU,在访问cache的时候,可能只需要1个HZ,而在访问DDR的时候,需要等待100Hz-200Hz才能返回结果是是很稀松平常的事。而连续的data barrier是可以很容易的将CPU性能打到30M的(单片机),3G Hz的CPU只有30MHz的性能是多么可怕的事情,为了在这100Hz中减少CPU流水停滞,CPU在设计上会尽可能的将load/store早的发出,在load/store返回之前执行与此无关的指令。但是从软件上考虑,这种顺序错位的load/store却是一个非常严重的问题,所以就有barrier出现,保障在某个指令点,指令执行会有确定的顺序。而这也就意味着,每次barrier出现,CPU都需要在30M Hz的性能上停留一段时间,所以CPU在设计上会不断地想要细化挖空barrier,使barrier发生时,CPU能多做一些指令。第一件事是上面说的barrier作用域的控制,作用域到达DDR和L3 cache是两回事,DDR可能需要消耗100Hz,而L3 cache则可能几个Hz就返回,所以很多需要“显像”到所有CPU上的时候,data barrier只需要到达L3 cache即可;第二件事就是限制data barrier的模式,除了i模式以外,其他模式都可以或多或少的“放过”一些load/store操作,而在程序的指令中,被“放过”的这些load/store可能会解锁很多条指令(当然,这还受到CPU执行指令深度设计的影响)。目前来看,作用域到L1/L3 cache的data barrier是非常快的,而作用域到DDR的data barrier从RISCV的波形来看是确实会产生不小的CPU停滞,当然这可能也与具体的RISCV CPU能力相关。
一点小思考
其实写到这里我们可以看到,当前数据barrier指令都是对全部load、全部store进行操作的,所以为了可以更多发射机会,是不是可以做一些对指定某个load-load指令对儿、指定某个load-store指令对儿的barrier?