NOC 总线

弄清了访存的路径,可能就会想到一个问题:处理器发出去的读写请求到底是个什么东西?要想搞清楚它,就需要引入总线。下文我拿ARM的AXI/ACE总线协议以及由它衍生的总线结构来展开讨论。这两个协议广泛用于主流的手机芯片上,是第四代AMBA(Advanced Microcontroller Bus Architecture)标准。

简单的总线就是一些地址线和数据线,再加一个仲裁器,就可以把处理器发过来的读写请求送到内存或者外设,再返回数据。在这个过程中,我们需要一个主设备,一个从设备,所有的传输都是主设备发起,从设备回应。让我们把处理器和它包含的缓存看作一个主设备,把内存控制器看作从设备。处理器发起访问请求,如果是读,那么总线把这个请求(包括地址)送到内存控制器,然后等待回应。过了一段时间,内存控制器把内存颗粒里面读出的数据交给总线,总线又把数据交给处理器。如果数据无误(ECC或者奇偶校验不出错),那么这个读操作就完成了。如果是写,处理器把写请求(包括地址)和数据交给总线,总线传递给内存控制器,内存控制器写完后,给出一个确认。这个确认经由总线又回到了处理器,写操作完成。

以上过程有几个重点。第一,处理器中的单个读指令,被分为了请求(地址),完成(数据)阶段。写指令也被分为了请求(地址,数据),完成(写入确认)阶段。第二,作为从设备,内存控制器永远都无法主动发起读写操作。如果一定要和处理器通讯,比如发生了读写错误,那就得使用中断,然后让处理器来发起读写内存控制器状态的请求。第三,未完成的读写指令就变成了OT,总线可以支持多个OT。然而,总线支持多OT并不表示处理器能发送这么多请求出来,尤其是读。所以瓶颈可能还是在处理器。

我遇到过几次这样的情况,在跑某个驱动的时候,突然系统挂死。但是别的设备中断还能响应,或者报个异常后系统又继续跑了。如果我们把上文的内存控制器替换成设备控制器,那就不难理解这个现象了。假设处理器对设备发起读请求,而设备没有回应,那处理器就会停在那等待。我看到的处理器,包括PowerPC, ARM,都没有针对这类情况的超时机制。如果没有中断,那处理器无法自己切换到别的线程(Linux等操作系统的独占模式),就会一直等待下去,系统看上去就挂住了。有些设备控制器可以自动探测这类超时,并通过中断调用相应的异常或者中断处理。在中断处理程序中,可以报个错,修改返回地址,跳过刚才的指令往下走,系统就恢复了。也有些处理器在触发某类异常后能自动跳到下一行指令,避免挂死。但是如果没有异常或者中断发生,那就永远挂在那。

继续回到总线。在AXI/ACE总线协议中,读和写是分开的通道,因为他们之间并没有必然联系。更细一些,总线上规定了五个组,分别是读操作地址(主到从),读操作数据(从到主),写操作地址(主到从),写操作数据(主到从),写操作确认(从到主)。读和写两大类操作之间,并没有规定先后次序。而在每一类操作之内的组之间,是有先后次序的,比如地址是最先发出的,数据随后,可以有很多拍,形成突发操作。而确认是在写操作中,从设备收到数据之后给出的。对内存控制器,必须在数据最终写入到颗粒之后再给确认,而不是收到数据放到内部缓存中的时候。

NOC 总线_第1张图片

对于同一个通道,如果收到连续的指令,他们之间的次序是怎么样的呢?AXI/ACE协议规定,次序可以打乱。拿读来举例,前后两条读指令的数据返回是可以乱序的。这里包含了一个问题,总线怎么区分住前后两读条指令?很简单,在地址和数据组里加几根信号,作为标志符,来区分0-N号读请求和完成。每一对请求和完成使用相同的标志符。有了这个标志符,就不必等前一个请求完成后才开始第二个请求,而是让他们交替进行,这样就可以实现总线的OT,极大提高效率。当然,也需要提供相应的缓冲来存储这些请求的状态。并且最大的OT数取决于缓冲数和标志符中小的那个。原因很简单,万一缓冲或者标志符用完了,但是所有的读操作全都是请求,没有一个能完成怎么办?那只好让新的请求等着了。于是就有了AXI/ACE总线的一条规则,同一个读或者写通道中,相同标志符的请求必须按顺序完成。

有时候,处理器也会拿这个标志符作为它内部的读写请求标志符,比如Cortex-A7就是这么干的。这样并不好,因为这就等于给自己加了限制,最大发出的OT不得大于总线的每通道标志符数。当一个处理器组里有四个核的时候,很可能就不够用了,人为限制了OT数。

最后,读写通道之间是没有规定次序的,哪怕标志相同。

看到这里可能会产生一个问题,读写指令里面有一个默认原则,就是相同地址,或者地址有重叠的时候,访存必须是顺序的。还有,如果访问的内存类型是设备,那么必须保证访存次序和指令一致。这个怎么在总线上体现出来呢?总线会检查地址来保证次序,一般是内存访问前后乱序地址不能64字节内,设备访问前后乱序地址不能在4KB内。

在AXI/ACE中,读和写通道的比例是一比一。实际上,在日常程序中,读的概率比写要大。当然,写缓存实际上伴随着缓存行填充linefill(读),而读缓存会造成缓存行移除eviction(写),再加上合并和次序调整,所以并不一定就是读写指令的比例。我看到Freescale PowerPC的总线CCB,读写通道的比率是二比一。我不知道为什么ARM并没有做类似的设计来提高效率,也许一比一也是基于手机典型应用统计所得出的最好比例。

至此,我们已经能够在脑海中想象一对读写通道中读写操作的传输情况了。那多个主从设备组合起来是怎么样的情况?是不是简单的叠加?这涉及到了总线设计最核心的问题,拓扑结构。

在ARM当前所有的总线产品里,根据拓扑的不同可以分为三类产品:NIC/CCI系列是交叉矩阵的(Crossbar),CCN系列是基于环状总线的(Ring),NoC系列是网状总线(Mesh)。他们各有特点,适合不同场景。交叉矩阵连接的主从设备数量受到限制,但是效率最高,读写请求可以在1到2个周期内就直达从设备。如下图所示,这就是一个5x4的交叉矩阵:

NOC 总线_第2张图片

根据我看到的数据,在28纳米制程上,5x4的配置下,这个总线的频率可以跑到300Mhz。如果进一步增加主从对数量,那么由于扇出增加,电容和走线增加,就必须通过插入更多的寄存器来增加频率。但这样一来,从主到从的延迟就会相应增加。哪怕就是保持5x3的配置,要想进一步提高到500Mhz,要么使用更好的工艺,16纳米我看到的是800Mhz;要么插入2-3级寄存器,这样,读写延时就会达到4-5个总线时钟周期,请求加完成来回总共需要10个。如果总线和处理器的倍频比率为1:2,那么仅仅是在总线上花费的时间,就需要至少20个处理器时钟周期。倍率为4,时间更长,40个时钟周期。要知道处理器访问二级缓存的延迟通常也不过10多个处理器周期。当然,可以通过增加OT数量减少平均延迟,可是由于处理器的OT数是有限制的,对于顺序处理器,可能也就是1-2个。所以,要达到更高的频率,支持更多的主从设备,就需要引入环状总线CCN系列,如下图:

NOC 总线_第3张图片

CCN总线上的每一个节点,除了可以和相邻的两个节点通讯之外,还可以附加两个节点组件,比如处理器组,三级缓存,内存控制器等。在节点内部,还是交叉的,而在节点之间,是环状的。这样使得总线频率在某种程度上摆脱了连接设备数量的限制(当然,还是受布线等因素的影响),在16纳米下,可以达到1.2GHz以上。当然,代价就是节点间通讯更大的平均延迟。为了减少平均延迟,可以把经常互相访问的节点放在靠近的位置。

在有些系统里,要求连接更多的设备,并且,频率要求更高。此时环状总线也不够用了。这时就需要NoC出马了,如下图:

NOC 总线_第4张图片

这个图中,刚才提到的交叉矩阵,可以作为整个网络的某部分。而连接整个系统的,是位于NoC内的节点。每个节点都是一个小型路由,它们之间传输的,是异步的包。这样,就不必维持路由和路由之间很大数量的连线,从而提高频率,也能支持更多的设备。当然,坏处就是更长的延迟。根据我看到的数据,在16纳米上,频率可以跑到1.5Ghz。并且它所连接每个子模块之间,频率和拓扑结构可以是不同的。可以把需要紧密联系的设备,比如CPU簇,GPU放在一个子网下减少通讯延迟。

不知你有没有发现,以上每一种总线,都可以在网络设备上找到原型。从设备直连,到环状局域网,到交换和路由。他们的拓扑结构是相通的。

在实际的ARM生态系统中,以上三种拓扑结构的使用情况是怎么样的呢?一般手机芯片上使用交叉矩阵,网络处理器和服务器上使用环状网络,而网状拓扑也被大量应用于手机芯片。最后一个的原因倒不是手机上需要连接的设备数太多,而是因为AXI/ACE协议都不支持一次传输拆分成多个,送给不同的内存控制器。这类传输有个名词,叫做interleaving,交叉访问。交叉访问有很多定义,从总线角度看,一种是一个主设备和一个从设备之间的多个读写请求之间的完成次序可以打乱,这个我们已经解释过。第二种是对于一个主设备的一次读写请求,把它分别送到多个从设备,并等待完成。为什么需要这种传输呢?因为在手机里面,GPU,显示控制器,视频控制器对内存带宽要求是很高的。一个1080p的屏幕,每秒要刷新60次,2百万个像素,每个像素32比特颜色,再加上8层图层,就需要4GB/s的数据。而一个1.6GHz传输率的DDR3控制器,64位数据,也只能提供10GB/s的的理论带宽。理论带宽和实际带宽由于各种因素的影响,会有很大差别,能做到70%的利用率就不错了。那处理器怎么办,其他各类控制器怎么办?只有增加内存控制器的数量,也就是多放几个从设备。但是,不能简单的增加数量。原因是,如果仅仅把不同的物理地址请求发送到不同的内存控制器上, 很可能在某段时间内,所有的物理地址全都是对应于其中某一个,还是不能满足带宽要求。解决方法就是,对于任何地址,都拆成多个请求,送到不同的内存控制器。并且这件事最好不是处理器来干(ARM的核都不支持拆分的读写),因为只有总线清楚有多少个内存控制器。最好处理器只管发请求,总线把所有请求拆分,并将数据收集完后,统一返回到处理器。

不幸的是,AXI/ACE总线天然就不支持一对多的交叉访问。原因很简单,会产生死锁。想象一下,有两个主设备,两个从设备,通过交叉矩阵连接。M1发送两个读请求,标志符都是1,先后送到到S1和S2,并等待完成。然后M2也做同样的事情,标志符都是2,先后送到S2和S1。此时,假设S2发现它如果把返回的数据次序交换一下,会更有效率,于是它就这么做了。但是M1却不能接收S2的返回数据,因为根据同标志符必须顺序完成的原则,它必须先等S1的返回数据。而S1此时也没法送数据给M2,因为M2也在等待S2返回的数据,死锁就出现了。解决方法是,任何一个主设备,不能同时发送请求到多个从设备。于是,就没法实现多内存控制器的交叉访问了。

而基于包传输的网状总线就没有这个问题,所有包都是异步的,不存在依赖关系。于是,出现了一家专门做NoC总线的公司Arteris,它们抓住这个机会,把支持多内存控制器同时访问的总线卖到了大部分做手机和平板芯片的公司。而ARM到目前为止,虽然也有了解决方案Arachne,但是还没什么实际应用。

现在的中低端手机很多都是8核,而根据ARM的设计,每个处理器组中最多有四个核。这就需要放两个处理器组在系统中,而他们之间的通讯,包括大小核的实现,就需要用到总线一致性。每个处理器组内部也需要一致性,原理和外部相同,我就不单独解释了。使用软件实可以现一致性,但是那样需要手动的把缓存内容刷到下一级缓存或者内存,对于一个64字节缓存行的64KB缓存来说,需要1000次刷新,每次就算是100纳秒,且OT=4的话,也需要25微秒。对处理器来说这是一个非常长的时间。ARM使用了一个协处理器来做这个事情,这是一个解决方案。为了用硬件解决,ARM引入了几个支持硬件一致性的总线,下图是第一代方案CCI400:

NOC 总线_第5张图片

CCI400是怎么做到硬件一致性的呢?简单来说,就是处理器组C1,发一个包含地址信息的特殊读写的命令到总线,然后总线把这个命令转给另一个处理器组C2。C2收到请求后,根据地址逐步查找二级和一级缓存,如果发现自己也有,那么就返回数据或者做相应的缓存一致性操作,这个过程称作snooping(监听)。具体的操作我不展开,ARM使用MOESI一致性协议,里面都有定义。在这个过程中,被请求的C2中的处理器核心并不参与这个过程,所有的工作由缓存和总线接口单元BIU等部件来做。为了符合从设备不主动发起请求的定义,需要两组主从设备,每个处理器组占一个主和一个从。这样就可以使得两组处理器互相保持一致性。而有些设备如DMA控制器,它本身不包含缓存,也不需要被别人监听,所以它只包含从设备,如上图桔黄色的部分。在ARM的定义中,具有双向功能的接口被称作ACE,只能监听别人的称作ACE-Lite。它们除了具有AXI的读写通道外,还多了个监听通道,如下图:

NOC 总线_第6张图片

多出来的监听通道,同样也有地址(从到主),回应(主到从)和数据(主到从)。每组信号内都包含和AXI一样的标志符,用来支持多OT。如果在主设备找到数据(称为命中),那么数据通道会被使用,如果没有,那告知从设备未命中就可以了,不需要传数据。由此,对于上文的DMA控制器,它永远不可能传数据给别人,所以不需要数据组,这也就是ACE和ACE-Lite的主要区别。

我们还可以看到,在读通道上有个额外的线RACK,它的用途是,当从设备发送读操作中的数据给主,它并不知道何时主能收到这个数据,因为我们说过插入寄存器会导致总线延迟变长。万一这个时候,对同样的地址A,它需要发送新的监听请求给主,就会产生一个问题:主是不是已经收到前面发出的地址A的数据了呢?如果没收到,那它可能会告知监听未命中。但实际上地址A的数据已经发给主了,它该返回命中。加了这个RACK后,从设备在收到主给的确认RACK之前,不会发送新的监听请求给主,从而避免了上述问题。写通道上的WACK同样如此。

我们之前计算过NIC400上的延迟,有了CCI400的硬件同步,是不是访问更快了呢?首先,硬件一致性的设计目的不是为了更快,而是软件更简单。而实际上,它也未必就快。因为给定一个地址,我们并不知道它是不是在另一组处理器的缓存内,所以无论如何都需要额外的监听动作。当未命中的时候,这个监听动作就是多余的,因为我们还是得从内存去抓数据。这个多余的动作就意味着额外的延迟,10加10一共20个总线周期,增长了100%。当然,如果命中,虽然总线总共上也同样需要10周期,可是从缓存拿数据比从内存拿快些,所以此时是有好处的。综合起来看,当命中大于一定比例,总体还是受益的。

可从实际的应用程序情况来看,除了特殊设计的程序,通常命中不会大于10%。所以我们必须想一些办法来提高性能。一个办法就是,无论结果是命中还是未命中,都让总线先去内存抓数据。等到数据抓回来,我们也已经知道监听的结果,再决定把哪边的数据送回去。这个办法的缺点,功耗增大,因为无论如何都要去读内存。第二,在内存访问本身就很频繁的时候,这么做会降低总体性能。

另外一个方法就是,如果预先知道数据不在别的处理器组缓存,那就可以让发出读写请求的主设备,特别注明不需要监听,总线就不会去做这个动作。这个方法的缺点就是需要软件干预,虽然代价并不大,分配操作系统页面的时候设下寄存器就可以,可是对程序员的要求就高了,必须充分理解目标系统。

CCI总线的设计者们还使用了一个新的方法来提高性能。他们在总线里加入一个监听过滤器(Snoop Filter)。这其实也是一块缓存(TAG RAM),把它所有处理器组内部一级二级缓存的状态信息都放在里面。数据缓存(DATA RAM)是不需要的,因为它只负责查看命中与否。这样做的好处就是,监听请求不必发到各组处理器,在总线内部就可以完成,省了将近10个总线周期,功耗也优于访问内存。它的代价是增加了一点缓存(一二级缓存的10%左右)。并且,如果监听过滤器里的某行缓存被替换(比如写监听命中,需要无效化(Invalidate)缓存行,MOESI协议定义),同样的操作必须在对应处理器组的一二级缓存也做一遍,以保持一致性。这个过程被称作反向无效化,它添加了额外的负担,因为在更新一二级缓存的时候,监听过滤器本身也需要追踪更新的状态,否则就无法保证一致性。幸好,在实际测试中发现,这样的操作并不频繁,一般不超过5%的可能性。当然,有些测试代码会频繁的触发这个操作,此时监听过滤器的缺点就显出来了。

以上的想法在CCI500中实现,示意图如下:

NOC 总线_第7张图片

在经过实际性能测试后,CCI设计人员发现总线瓶颈移到了访问这个监听过滤器的窗口,这个瓶颈其实掩盖了上文的反向无效化问题,它总是先于反向无效化被发现。把这个窗口加大后,又在做测试时发现,如果每个主从接口都拼命灌数据(主从设备都是OT无限大,并且一主多从有前后交叉),在主从设备接口处经常出现等待的情况,也就是说,明明数据已经准备好了,设备却来不及接收。于是,又增加了一些缓冲来存放这些数据。其代价是稍大的面积和功耗。请注意,这个缓冲和存放OT的状态缓冲并不重复。

根据实测数据,在做完所有改进后,新的总线带宽性能同频增加50%以上。而频率可以从500Mhz提高到800Mhz。当然这个结果只是一个模糊的统计,如果我们考虑处理器和内存控制器OT数量有限,被监听数据的百分比有不同,命中率有变化,监听过滤器大小有变化,那肯定会得到不同的结果。

作为一个手机芯片领域的总线,需要支持传输的多优先级也就是QoS。因为显示控制器等设备对实时性要求高,而处理器组的请求也很重要。支持QoS本身没什么困难,只需要把各类请求放在一个缓冲,根据优先级传送即可。但是在实际测试中,发现如果各个设备的请求太多太频繁,缓冲很快就被填满,从而阻塞了新的高优先级请求。为了解决这个问题,又把缓冲按优先级分组,每一组只接受同等或更高优先级的请求,这样就避免了阻塞。这些方法和网络防拥塞设计如出一辙。

此外,为了支持多时钟和电源域,使得每一组处理器都可以动态调节电压和时钟频率,CCI系列总线还可以搭配异步桥ADB(Asynchronous Domain Bridge)。它对于性能有一定的影响,在倍频是2的时候,信号穿过它需要一个额外的总线时钟周期。如果是3,那更大些。在对于访问延迟有严格要求的系统里面,这个时间不可忽略。如果不需要额外的电源域,我们可以不用它,省一点延迟。NIC/CCI/CCN/NoC总线天然就支持异步传输。

和一致性相关的是访存次序和锁,有些程序员把它们搞混了。假设我们有两个核C0和C1。当C0和C1分别访问同一地址A0,无论何时,都要保证看到的数据一致,这是一致性。然后在C0里面,它需要保证先后访问地址A0和A1,这称作访问次序,此时不需要锁,只需要壁垒指令。如果C0和C1上同时运行两个线程,当C0和C1分别访问同一地址A0,并且需要保证C0和C1按照先后次序访问A0,这就需要锁。所以,单单壁垒指令只能保证单核单线程的次序,多核多线程的次序需要锁。而一致性保证了在做锁操作时,同一变量在缓存或者内存的不同拷贝,都是一致的。

ARM的壁垒指令分为强壁垒DSB和弱壁垒DMB。我们知道读写指令会被分成请求和完成两部分,强壁垒要求上一条读写指令完成后才能开始下一个请求,弱壁垒则只要求上一条读写指令发出请求后就可以继续下一条读写指令的请求,且只能保证,它之后的读写指令完成时,它之前的读写指令肯定已经完成了。显然,后一种情况性能更高,OT>1。但测试表明,多个处理器组的情况下,壁垒指令如果传输到总线,只能另整体系统性能降低,因此在新的ARM总线中是不支持壁垒的,必须在芯片设计阶段,通过配置选项告诉处理器自己处理壁垒指令,不要送到总线。但这并不影响程序中的壁垒指令,处理器会在总线之前把它过滤掉。

具体到CCI总线上,壁垒机制是怎么实现的呢?首先,壁垒和读写一样,也是使用读写通道的,只不过它地址总是0,且没有数据。标志符也是有的,此外还有额外的2根线BAR0/1,表明本次传输是不是壁垒,是哪种壁垒。他是怎么传输的呢?先看弱壁垒,如下图:

NOC 总线_第8张图片

Master0写了一个数据data,然后又发了弱壁垒请求。CCI和主设备接口的地方,一旦收到壁垒请求,立刻做两件事,第一,给Master0发送壁垒响应;第二,把壁垒请求发到和从设备Slave0/1的接口。Slave1接口很快给了壁垒响应,因为它那里没有任何未完成传输。而Slave0接口不能给壁垒响应,因为data还没发到从设备,在这条路径上的壁垒请求必须等待,并且不能和data的写请求交换次序。这并不能阻挠Master0发出第二个数据,因为它已经收到它的所有下级(Master0接口)的壁垒回应,所以它又写出了flag。如下图:

NOC 总线_第9张图片

此时,flag在Master0接口中等待它的所有下一级接口的壁垒响应。而data达到了Slave0后,壁垒响应走到了Master0接口,flag继续往下走。此时,我们不必担心data没有到slave0,因为那之前,来自Slave0接口的壁垒响应不会被送到Master0接口。这样,就做到了弱壁垒的次序保证,并且在壁垒指令完成前,flag的请求就可以被送出来。

对于强壁垒指令来说,仅仅有一个区别,就是Master0接口在收到所有下一级接口的壁垒响应前,它不会发送自身的壁垒响应给Master0。这就造成flag发不出来,直到壁垒指令完成。如下图:

NOC 总线_第10张图片

这样,就保证了强壁垒完成后,下一条读写指令才能发出请求。此时,强壁垒前的读写指令肯定是完成了的。

另外需要特别注意的是,ARM的弱壁垒只是针对显式数据访问的次序。什么叫显式数据访问?读写指令,缓存,TLB操作都算。相对的,什么是隐式数据访问?在处理器那一节,我们提到,处理器会有推测执行,预先执行读写指令;缓存也有硬件预取机制,根据之前数据访问的规律,自动抓取可能用到的缓存行。这些都不包含在当前指令中,弱壁垒对他们无能为力。因此,切记,弱壁垒只能保证你给出的指令次序,并不能保证在它们之间没有别的模块去访问内存,哪怕这个模块来自于同一个核。

简单来说,如果只需要保证读写次序,用弱壁垒;如果需要某个读写指令完成才能做别的事情,用强壁垒。以上都是针对普通内存类型。当我们把类型设成设备时,自动保证强壁垒。

我们提到,壁垒只是针对单核。在多核多线程时,哪怕使用了壁垒指令,也没法保证读写的原子性。解决办法有两个,一个是软件锁,一个是原子操作。原子操作我看到过的有两种,一种是总线收到请求时,直接封掉整个总线,同时只有一个核能访问。这样效率很低。还有个方法是把锁的请求发送到对端设备,比如内存控制器,让他禁止别的核的访问,而总线依然可以运行,这样效率就高不少,我看到过的数据,减少10倍时间。但是AXI/ACE协议不支持原子操作。所以需要用到软件锁。

软件锁中有个自旋锁,能用一个ARM硬件机制exclusive access来实现。当使用特殊指令对一个地址写入值,相应缓存行上会做一个特殊标记,表示还没有别的核去写这行缓存。然后下条指令读这个行,如果标记没变,说明写和读之间没有人打扰,那么就拿到锁了。如果变了,那么回到写的过程重新获取锁。由于缓存一致性,这个锁变量可以被多个核与线程使用。当然,过程中还是需要壁垒指令来保证次序。

对于普通内存,还会产生一个问题,就是读写操作可能会经过缓存,你不知道数据是否最终写到了内存中。通常我们使用clean操作来刷缓存。但是刷缓存本身是个模糊的概念,缓存存在多级,有些在处理器内,有些在总线之后,到底刷到哪里算是终结呢?还有,为了保证一致性,刷的时候是不是需要通知别的处理器和缓存?为了把这些问题规范化,ARM引入了Point of Unification/Coherency,Inner/Outer Cacheable和System/Inner/Outer/Non Shareable的概念。

NOC 总线_第11张图片

PoU是指,对于某一个核Master,附属于它的指令,数据缓存和TLB,如果在某一点上,它们能看到一致的内容,那么这个点就是PoU。如上图右侧,MasterB包含了指令,数据缓存和TLB,还有二级缓存。指令,数据缓存和TLB的数据交换都建立在二级缓存,此时二级缓存就成了PoU。而对于上图左侧的MasterA,由于没有二级缓存,指令,数据缓存和TLB的数据交换都建立在内存上,所以内存成了PoU。

PoC是指,对于系统中所有Master(注意是所有的,而不是某个核),如果存在某个点,它们的指令,数据缓存和TLB能看到同一个源,那么这个点就是PoC。如上图右侧,二级缓存此时不能作为PoC,因为MasterB在它的范围之外,直接访问内存。所以此时内存是PoC。在左图,由于只有一个Master,所以内存是PoC。

再进一步,如果我们把右图的内存换成三级缓存,把内存接在三级缓存后面,那PoC就变成了三级缓存。

有了这两个定义,我们就可以指定缓存操作和读写指令到底发到哪个范围。比如在下图的系统上,有两组A15,每组四个核,组内含二级缓存。系统的PoC在内存,而A15的PoU分别在它们自己组内的二级缓存上。在某个A15上执行Clean清指令缓存,范围指定PoU。显然,所有四个A15的一级指令缓存都会被清掉。那么其他的各个Master是不是受影响?那就要用到Inner/Outer/Non Shareable。

NOC 总线_第12张图片

Shareable的很容易理解,就是某个地址的可能被别人使用。我们在定义某个页属性的时候会给出。Non-Shareable就是只有自己使用。当然,定义成Non-Shareable不表示别人不可以用。某个地址A如果在核1上映射成Shareable,核2映射成Non-Shareable,并且两个核通过CCI400相连。那么核1在访问A的时候,总线会去监听核2,而核2访问A的时候,总线直接访问内存,不监听核1。显然这种做法是错误的。

对于Inner和Outer Shareable,有个简单的的理解,就是数据共享属于Outer Shareable;指令和TLB等只读的共享,属于Inner Shareable,也属于Outer Shareable。在上图的系统中,A15和A7共享指令,它们之间是Inner Shareable。当然,他们数据也是共享的,所以也是Outer Shareable。而DMA, Mali和处理器之间,就只有数据是共享的,所以不是Inner Shareable,而是Outer Shareable。

我们在集成A15/A7等ARM处理器到SoC系统的时候,是需要设置Inner和Outer Shareable属性的。设了Inner,就表示指令操作和指令缓存,TLB等只读的访问会被送到总线。设了Outer,就表示数据操作,数据缓存的访问会被送到总线。且如果设了Outer,必须设置Inner。反之不必。正常情况是这两个都设上。

说了这么多概念,你可能会想这有什么用处?回到上文的Clean指令,PoU使得四个A7的指令缓存中对应的行都被清掉。由于是指令缓存操作,Inner Shareable属性使得这个操作被扩散到总线。而CCI400总线会把这个操作广播到所有可能接受的口上。ACE口首当其冲,所以四个A15也会清它们对应的指令缓存行。对于Mali和DMA控制器,他们是ACE-Lite,本不必清。但是请注意它们还连了DVM接口,专门负责收发缓存维护指令,所以它们的对应指令缓存行也会被清。不过事实上,它们没有对应的指令缓存,所以只是接受请求,并没有任何动作。

你可能又会想,我们要这么复杂的定义有什么用?用处是,精确定义缓存维护和读写指令的范围。如果我们改变一下,总线不支持Inner/Outer Shareable的广播,那么就只有A7处理器组会清缓存行。显然这么做在逻辑上不对,因为A7/A15可能运行同一行代码。并且,我们之前提到过,如果把读写属性设成Non-Shareable,那么总线就不会去监听其他主,减少访问延迟,这样可以非常灵活的提高性能。

再回到前面的问题,刷某行缓存的时候,怎么知道数据是否最终写到了内存中?对不起,非常抱歉,还是没法知道。你只能做到把范围设成PoC。如果PoC是三级缓存,那么最终刷到三级缓存,如果是内存,那就刷到内存。不过这在逻辑上没有错,按照定义,所有Master如果都在三级缓存统一数据的话,那就不必刷到内存了。

简而言之,PoU/PoC定义了指令和命令的所能抵达的缓存或内存,在到达了指定地点后,Inner/Outer Shareable定义了它们被广播的范围。

再来看看Inner/Outer Cacheable,这个就简单了,仅仅是一个缓存的前后界定。一级缓存一定是Inner Cacheable的,而最外层的缓存,比如三级,可能是Outer Cacheable,也可能是Inner Cacheable。他们的用处在于,在定义内存页属性的时候,可以在不同层的缓存上有不同的处理策略。

在ARM的处理器和总线手册中,还会出现几个PoS(Point of Serialization)。它的意思是,在总线中,所有主设备来的各类请求,都必须由控制器检查地址和类型,如果存在竞争,那就会进行串行化。这个概念和其他几个没什么关系。

纵观整个总线的变化,还有一个核心问题并没有被提及,那就是动态规划re-scheduling与合并Merging。处理器和内存控制器中都有同样的模块,专门负责把所有的传输进行分类,合并,调整次序,甚至预测未来可能接收到的读写请求地址,以实现最大效率的传输。这个问题在分析性能时会重新提到。但是在总线这层,软件能起的影响很小。清楚了总线延迟和OT最大的好处是可以和性能计数器的统计结果精确匹配,看看是不是达到预期了。

你可能感兴趣的:(FPGA,RISC-V,NOC)