CPU是怎样访问内存的?简单的答案是,CPU执行一条访存指令,把读写央求发往内存管理单元。内存管理单元中止真假转换,把命令发往总线。总线把命令传送给内存控制器,内存控制器再次翻译地址,对相应内存颗粒中止存取。之后,读取的数据写入确认依照原路返回。再复杂些,当中插入多级缓存,在每一层缓存都未命中的状况下,访问才会最终抵达内存颗粒。 知道了完好的途径,开端研讨每一步中的硬件到底是怎样样的,读写指令到底是怎样在其中传输的。要了解硬件,首先要说下处置器。处置器的基本结构并不复杂,普通分为取指令、译码、发射、执行、写回五个步骤。而我们说的访存,指的是访问数据,不是指令抓取。访问数据的指令在前三步没有什么特殊,在第四步,它会被发送到存取单元,等候完成。当指令在存取单元里的时分,产生了一些有趣的问题。 第一个问题,关于读指令,当处置器在等候数据从缓存或者内存返回的时分,它到底是什么状态?是等在那不动呢,还是继续执行别的指令? 普通来说,假如是乱序执行的处置器,那么能够执行后面的指令,假如是次第执行,那么会进入停顿状态,直到读取的数据返回。当然,这也不是绝对的。在举反例之前,我们先要弄清什么是乱序执行。乱序执行是说,关于一串给定的指令,为了进步效率,处置器会找出非真正数据依赖的指令,让他们并行执行。但是,指令执行结果在写回到寄存器的时分,必须是次第的。也就是说,哪怕是先被执行的指令,它的运算结果也是依照指令次序写回到最终的寄存器的。这个和很多程序员了解的乱序执行是有区别的。我发现有些人在调试软件问题的时分,会觉得运用了一个乱序的处置器,那么可能会使得后面的代码先被执行,从而让调试无法中止。 他们搞混了两个概念,就是访存次序和指令完成次序。关于普通的运算指令,他们仅仅在处置器内部执行,所以你看到的是写回次序。而关于访存指令,指令会产生读央求,并发送四处置器外部,你看到的次序是访存次序。关于乱序处置器,可能同时存在多个央求,而其次序,是打乱的,不按原指令次第的。但是此时,这些被发送到外部的读央求,并没有拿到返回结果,指令也没有完成。所以,这并不违背乱序执行次第完成的准绳。假如有前后两条读指令,没有数据相关性,哪怕是后面那条读的数据先被返回,它的结果也不能先写回到最终的寄存器,而是必须等到前一条完成后才能够。 关于次第执行的处置器,同样是两条读指令,普通必须等到前一条指令完成,才干执行第二条,所以在处置器外部看到的是按次序的访问。不外也有例外,好比读写同时存在的时分,由于读和写指令实践上走的是两条途径,所以可能会看到同时存在。 还有,次第处置器上,哪怕是两条读指令,也有可能同时存在两个外部央求。好比Cortex-A7,关于连续的读指令,在前一条读未命中一级缓存,到下一级缓存或者内存抓取数据的时分,第二条读指令能够被执行。所以说,乱序和次第并不直接影响指令执行次序。他们的区别在于,乱序需求额外的缓冲和逻辑块(称为重排序缓冲, re-order buffer)来计算和存储指令间的相关性以及执行状态,而次第处置器没有重排序缓冲,或者十分简单。这些额外的面积可不小,据我所看到的,能够占四处置器中心的40%。它们所带来的更高的并行度,性能提升却一定有40%。由于我们写的单线程程序,由于存在很多数据相关,构成指令的并行是有限的,再大的重排序缓冲也处置不了真正的数据相关。所以关于功耗敏感的处置器还是运用次第执行。 还有一点需求留意,次第执行的处置器,在指令抓取,解码和发射阶段,两条或者多条指令,是能够同时中止的。好比,无依赖关系的读指令和运算指令,能够被同时发射到不同的执行单元,同时开端执行。但是完成还是按次第的。 但是,在有些ARM处置器上,好比Cortex-A53,向量或者加解密指令是能够乱序完成的,这类运算的结果之间并没有数据依赖性。这点请千万留意。 再来看看写指令。写和读有个很大的不同,就是写指令不用等候数据写到缓存或者内存,就能够完成了。写进来的数据会到一个叫做store buffer的缓冲,它位于一级缓存之前,只需它没满,处置器就能够直接往下走,不用中止并等候。所以,关于连续的写指令,无论次第还是乱序执行处置器,都可能看到多个写央求同时挂在处置器总线上。同时,由于处置器不用像读指令那样等候结果,就能够在单位时间内送出更多写央求,所以我们能够看到写带宽通常是大于读带宽的。 以上所说的读写访问都是在开启缓存的状况。 关于同时存在的多个央求,有一个名词来定义它,叫做outstanding transaction,简称OT。它和延迟一同,构成了我们对访存性能的描画。延迟这个概念,在不同范畴有不同的定义。在网络上,网络延迟表示单个数据包从本地动身,经过交流和路由,抵达对端,然后返回,当中所花的总时间。在处置器上,我们也能够说读写的延迟是指令发出,经过缓存,总线,内存控制器,内存颗粒,然后原路返回所破费的时间。但是,更多的时分,我们说的访存延迟是大量读写指令被执行后,统计出来的平均访问时间。这里面的区别是,当OT=1的时分,总延时是简单累加。当OT>1,由于同时存在两个访存并行,总时间通常少于累加时间,并且能够少很多。这时分得到的平均延迟,也被称作访存延迟,并且用得更普遍。再精确一些,由于多级流水线的存在,假定流水线每一个阶段都是一个时钟周期,那访问一级缓存的平均延迟其实就是一个周期.而关于后面的二级,三级缓存和内存,就读指令来说,延迟就是从指令被发射(留意,不是从取指)到最终数据返回的时间,由于处置器在执行阶段等候,流水线起不了作用。假如OT=2, 那么时间可能缩短将近一半。OT>1的益处在这里就表示出来了。当然,这也是有代价的,存储未完成的读央求的状态需求额外的缓冲,而处置器可能也需求支持乱序执行,构成面积和功耗进一步上升。关于写指令,只需store buffer没满,还是一个时钟周期。当然,假如流水线上某个节拍大于一个时钟周期,那平均的延时就会取决于这个最慢的时间。在读取二级,三级缓存和内存的时分,我们能够把等候返回看作一个节拍,那么就能很自然的了解此时的延迟了。由此,我们能够得到每一级缓存的延迟和访存延迟。 上图画了读写指令经过的单元。我把流程简单描画下: 当写指令从存取单元LSU动身,它首先经过一个小的store queue,然后进入store buffer。之后,写指令就能够完成了,处置器不用等候。Store buffer通常由几个8-16字节的槽位组成,它会对自己收到的每项数据中止地址检查,假如能够兼并就兼并,然后发送央求到右边的一级缓存,请求分配一行缓存,来寄存数据,直到收到响应,这称作写分配write allocate。当然,等候的过程能够继续兼并同缓存行数据。假如数据是Non-Cacheable的,那么它会计算一个等候时间,然后把数据兼并,发送到总线接口单元BIU里面的写缓冲Write buffer。 而写缓冲在把数据发到二级缓存之前,会经过监听控制单元,把四个核的缓存做分歧性。过程和总线描画的相似,就未几讲了。 当读指令从存取单元LSU动身,无论能否Cacheable的,都会经过一级缓存。假如命中,那么直接返回数据,读指令完成。假如未命中,那么Non-Cacheable的央求直接被送到Read Buffer。假如是Cacheable的,那么一级缓存需求分配一个缓存行,并且把原来的数据写出到交流缓冲eviction buffer,同时发起一个缓存行填充,发送到Linefill Buffer。eviction buffer会把它的写出央求送到BIU里面的Write buffer,和Store Buffer送过来的数据一同,发到下一级接口。然后这些央求又经过监听控制单元做分歧性检测后,发到二级缓存。当然有可能读取的数据存在于别的处置器一级缓存,那么就直接从那里抓取。 过程并不复杂,但程序员关怀的是这个过程的瓶颈在哪,对读写性能影响如何。我们曾经解释过,关于写,由于它能够立刻完成,所以它的瓶颈并不来自于存取单元;关于读,由于处置器会等候,所以我们需求找到读取途径每一步能发出多少OT,每个OT的数据长度是多少。 拿Cortex-A7来举例,它有2x32字节linefill buffer,支持有条件的miss-under-miss(相邻读指令必须在3时钟周期内),也就是OT最多等于2,而它的数据缓存行长度是64字节,所以每个OT都是半个缓存行长度。关于Cacheable的读来说,我还关怀两个数据,就是eviction buffer和Write buffer,它们总是随同着line fill。在A7中,存在一个64字节的eviction buffer和一个Write buffer。有了这些条件,那么我就能够说,关于连续的读指令,我能做到的OT就是2,而linefill的速度和eviction,write buffer的速度分歧,由于2x32=64字节。 那这个结论是不是正确?写个小程序测试下就知道。我们能够关掉二级缓存,保存一级缓存,然后用以下指令去读取一个较大的内存区域。一切的地址都是缓存行对齐,对齐的意义我就不说了,错误齐,以至越过缓存行边疆,会把一个操作变成两个,肯定会慢。伪代码如下: loopload R0, addr+0load R0, addr+4load R0, addr+8load R0, addr+12addr=addr+16 这里经过读取指令不时地去读数据。经过处置器自带的性能计数器看了下一级缓存的未命中率,6%多一点。这恰恰是4/64字节的比率。阐明关于一个新的缓存行,第一个四字节总是未命中,然后面15个四字节总是命中。当然,细致的延迟和带宽还和总线,内存控制器有关,这里只能经过命中率简单考证下。 关于有的处置器,是严厉次第执行的,没有A7那样的miss-under-miss机制,所以OT=1。我在Cortex-R5上做同样的实验,它的缓存行长度是32字节,2xLinefill buffer是32字节。测试得到的命中率是12%多点。也完整契合预算。 但是为什么R5要设计两个32字节长度的Linefill buffer?既然它的OT=1,多出来的一个岂不是没用?实践上它是能够被用到的,而措施就是运用预取指令PLD。预取指令的特性就是,它被执行后,处置器同样不用等候,而这个读央求会被同样发送到一级缓存。等到下次有读指令来真正读取同样的缓存行,那么就可能发现数据曾经在那了。它的地址必须是缓存行对齐。这样,读也可像写那样把第二个 Linefill buffer给用上了。 我们把它用到前面的例子里: loopPLD addr+32load R0, addr+0;...;load R0, addr+28;load R0, addr+32;...;load R0, addr+60;addr=addr+64 PLD预先读取第二行读指令的地址。测试发现,此时的未命中率还是6%。这也契合预算,由于第二排的读指令总是命中,第一排的未命中率4/32,平均下就是6%。而测试带宽提升了80%多。单单看OT=2,它应该提升100%,但实践不可能那么理想化,80%也能够了解。 还有一种机制使得OT能够更大,那就是缓存的硬件预取。当程序访问连续的或者有规律的地址时,缓存会自动检测出这种规律,并且预先去把数据取来。这种措施同样不占用处置器时间,但是也会占用linefill buffer,eviction buffer和write buffer。所以,假如这个规律找的不好,那么反而会降低效率。 读看完了,那写呢?Cacheable的写,假如未命中缓存,就会引发write allocate,继而构成Linefill和eviction,也就是读操作。这点可能很多程序员没想到。当存在连续地址的写时,就会随同着一连串的缓存行读操作。有些时分,这些读是没有意义的。好比在memset函数中,能够直接把数据写到下一级缓存或者内存,不需求额外的读。于是,大部分的ARM处置器都完成了一个机制,当探测到连续地址的写,就不让store buffer把数据发往一级缓存,而是直接到write buffer。并且,这个时分,更容易兼并,构成突发写,进步效率。在Cortex-A7上它被称作Read allocate方式,意义是取消了write allocate。而在有的处置器上被称作streaming方式。很多跑分测试都会触发这个方式,因而能在跑分上更有优势。 但是,进入了streaming方式并不意味着内存控制器收到的地址都是连续的。想象一下,我们在测memcpy的时分,首先要从源地址读数据,发进来的是连续地址,并且是基于缓存行的。过了一段时间后,缓存都被用完,那么eviction呈现了,并且它是随机或者伪随机的,写进来的地址并无规律。这就打断了原本的连续的读地址。再看写,在把数据写到目的地址时,假如连续的写地址被发现,那么它就不会触发额外的linefill和eviction。这是好事。可是,直接写到下一级缓存或者内存的数据,很有可能并不是完好的缓存发突发写,应为store buffer也是在不时和write buffer交互的,而write buffer还要同时接受eviction buffer的央求。其结果就是写被分红几个小段。这些小块的写地址,eviction的写地址,混合着读地址,让总线和内存控制器增加了担负。它们必须采用适合的算法和参数,才干兼并这些数据,更快的写到内存颗粒。 但是事情还没有完。我们刚才提到,streaming方式是被触发的,同样的,它也能够退出。退出条件普通是发现存在非缓存行突发的写。这个可能受write buffer的响应时间影响。退出后,write allocate就又恢复了,从而读写地址愈加不连续,内存控制器愈加难以优化,延时进一步增加,反响四处置器,就更难坚持在streaming方式。 再进一步,streaming方式其实存在一个问题,那就是它把数据写到了下一级缓存或者内存,万一这个数据马上就会被运用呢?那岂不是还得去抓取?针对这个问题,在ARM v8指令集中(适用于A53/57/72),又引入了新的一条缓存操作指令DCZVA,能够把整行缓存设成0,并且不引发write allocate。为什么?由于整行数据都被要改了,而不是某个字段被改,那就没有必要去把原来的值读出来,所以只需求allocate,不需求读取,但它还是会引发eviction。相似的,我们也能够在运用某块缓存前把它们整体肃清并无效化,clean&invalidate,这样就不会有eviction。不外假如测试数据块足够大,这样只是相当于提早做了eviction,并不能消弭,让写集中在某段。使之后的读更连续。 以上都是针对一级缓存。二级缓存的控制力度就小些,代码上无法影响,只能经过设置寄存器,翻开二级缓存预取或者设置预取偏移。我在ARM的二级缓存控制器PL301上看到的,假如偏移设置的好,抓到的数据正好被用上,能够在代码和一级缓存优化完成的基础上,读带宽再提升150%。在新的处置器上,同时能够有多路的预取,探测多组访存模板,进一步进步效率。并且,每一级缓存后面挂的OT数目肯定大于上一级,它包含了各类读写弛缓存操作,应用好这些OT,就能进步性能。 关于Non-Cacheable的写,它会被store buffer直接送到write buffer中止兼并,然后到下一级缓存。关于Non-Cacheable的读,我们说过它会先到缓存看看是不是命中,未命中的话直接到read buffer,兼并后发往下一级缓存。它通常不占用linefill buffer,由于它通常是4到8字节,不需求运用缓存行大小的缓冲。 我们有时分也能够应用Non-Cacheable的读通道,和Cacheable的读操作并行,进步效率。它的原理就是同时应用linefill buffer和read buffer。此时必须保障处置器有足够的OT,不停顿。 简而言之,访存的软件优化的准绳就是,坚持对齐,找出更多可应用的OT,访存和预取混用,坚持更连续的访问地址,缩短每一环节的延迟。 最后解释一下缓存延迟的产生缘由。程序员可能不知道的是,不同大小的缓存,他们能抵达的时钟频率是不一样的。ARM的一级缓存,16纳米工艺下,大小在32-64K字节,能够跑在1-2Ghz左右,和处置器同频。处置器频率再快,那么访问缓存就需求2-3个处置器周期了。而二级缓存更慢,256K字节的,能有800Mhz就很好了。这是由于缓存越大,需求查找的目录index越大,扇出fanout和电容越大,自然就越慢。还有,通常处置器宣传时分所说的访问缓存延迟,存在一个前提,就是运用虚拟地址索引VIPT。这样就不需求查找一级Tlb表,直接得到索引地址。假如运用物理地址索引PIPT,在查找一级tlb中止真假转换时,需求额外时间不说,假如产生未命中,那就要到二级以至软件页表去找。那显然太慢了。那为什么不全运用VIPT呢?由于VIPT会产生一个问题,多个虚地址会映射到一个实地址,从而使得缓存多个表项对应一个实地址。存在写操作时,多条表项就会惹起分歧性错误。而指令缓存通常由于是只读的,不存在这个问题。所以指令缓存大多运用VIPT。随着处置器频率越来越高,数据缓存也只能运用VIPT。为理处置前面提到的问题,ARM在新的处置器里面加了额外的逻辑来检测重复的表项。 啰嗦了那么多,该说下真正系统里的访存延迟到底如何了。直接上图: 上图的配置中,DDR4跑在3.2Gbps,总线800Mhz,内存控制器800Mhz,处置器2.25Ghz。关掉缓存,用读指令测试。延迟包含出和进两个方向,69.8纳秒,这是在总是命中一个内存物理页的状况下的最优结果,随机的地址访问需求把17.5纳秒再乘以2到3。关于物理页的解释请参看内存一章。 在内存上花的时间是控制器+物理层+接口,总共38.9纳秒。百分比55%。假如是访问随机地址,那么会超越70纳秒,占70%。在总线和异步桥上花的时间是20纳秒,8个总线时钟周期,28%。处置器11.1纳秒,占16%,20个处置器时钟周期。 所以,即便是在3.2Gbps的DDR4上,大部分时间还都是在内存,显然优化能够从它上面入手。在处置器中的时间只需一小部分。但从另外一个方面,处置器控制着linefill,eviction的次数,地址的连续性,以及预取的效率,固然它自己所占时间最少,但也是优化的重点。 在ARM的道路图上,还呈现了一项并不算新的技术,称作stashing。它来自于网络处置器,原理是外设控制器(PCIe,网卡)向处置器发送央求,把某个数据放到缓存,过程和监听snooping很相似。在某些范畴,这项技术能够惹起质的变更。举个例子,intel至强处置器,配合它的网络转发库DPDK,能够做到平均80个处置器周期接受从PCIe网卡来的包,解析包头后送还回去。80周期是个什么概念?看过了上面的访存延迟图后你应该有所了解,处置器访问下内存都需求200-300周期。而这个数据从PCIe口DMA到内存,然后处置器抓取它中止处置后,又经过DMA从PCIe口进来,整个过程肯定大于访存时间。80周期的平均时间阐明它肯定被提早送到了缓存。 但传进来的数据很多,只需PCIe或者网卡控制器才知道哪个是包头,才干精确的推送数据,不然缓存会被无用的数据淹没。这个过程做好了,能够让软件处置以太网或者存储单元的速度超越硬件加速器。事实上,在freescale的网络处置器上,有了硬件加速器的辅佐,处置包的平均延迟需求200处置器周期,曾经慢于至强了。 还有,在ARM新的面向网络和效劳器的中心上,会呈现一核两线程的设计。处置包的任务自然合适多线程,而一核两线程能够更有效的应用硬件资源,再加上stashing,如虎添翼。(转自玩转单片机) 1. 免责声明:本文系网络转载,版权归原作者一切。如触及作品版权问题,请与我们联络,我们将依据您提供的版权证明资料确认版权并支付稿酬或者删除内容。 |