ARM和RISCV的barrier指令设计的思考 2024.3

写的还是不够深刻,需要把Processor Core的Pipeline纳入进来


前言

对比着看一下ARM和RISCV两个指令架构下面关于barrier的相关指令,有很多ARM的指令透露出一些成熟的思考。

先罗列一下ARM和RISCV的barrier:

ARM Barrier InstructionRISCV Barrier Instruction
  • ISB(Instruction Synchronization Barrier)
  • DMB(Data Memory Barrier)
  • DSB(Data Synchronization Barrier)

                DMB和DSB有一些参数

                OSHLD

                OSHST

                OSH

                NSHLD

                NSHST

                NSH

                ISHLD

                ISHST

                ISH

                LD

                ST

                

  • LDAR(Load-Acquire)
  • STLR(Store-Release)
  • SB(Speculation Barrier)
  • CSDB(Consumption of Speculative Data Barrier)
  • SSBB(Speculative Store Bypass Barrier)
  • PSB CSYNC(Profiling Synchronization Barrier)
  • PSSBB (Physical Speculative Store Bypass Barrier)
  • TSB CSYNC (Trace Synchronization Barrier)
  • fence [iorw] [iorw]

                i:设备内存输入(读取)

                o: 设备内存输出(写入)

                r:普通内存读取

                w:普通内存写入

  • 原子操作中蕴含的load aquire/store release

                amo{op}.{w/d}.{rl/aq/aqrl}

  • fence.i

                之后从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这两段指令序列却是保障了顺序的。再进一步解释:虽然instructions0instructions1这两段指令序列是不保证顺序,但是如果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?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值