When chip manufacturers introduce products to customers, the most important things to talk about from a hardware perspective are functions, performance, power consumption and price. The function mainly depends on what interfaces the chip provides, such as flash memory, memory, PCIe, USB, SATA, Ethernet, etc. It also depends on the internal computing modules, such as floating point devices, decoders, encryption and decryption, graphics accelerators, and network accelerators. etc. Performance, for a CPU, means how many points a test program can run, such as Dhrystone, Coremark, SPEC2000/2006, etc. For different applications, such as mobile phones, we will also look at the running scores of the graphics processor; for example, the network will also look at the packet forwarding rate. Of course, customers will also run some of their own typical applications to get a more accurate performance evaluation. Power consumption is how many watts of power the entire chip consumes when running a certain program. Usually, the processor will run at the highest frequency at this time, but this does not mean that all transistors are working. Due to the existence of powergating and clock gating, those unused logic and on-chip memory blocks are not completely consuming power. . When I see the maximum power consumption of processors given by chip companies, they are usually running Dhrystone. This program has a special feature. It only runs on the first-level cache and does not access the second-level cache or memory. What you get in this way is actually not the true maximum power consumption. But from actual experience, no application can make the CPU consume higher energy, so there is nothing wrong with measuring the maximum power consumption in this way. Of course, the overall chip power consumption must also include various accelerators and interfaces, especially the modules that will be used.
When designing an SoC, performance, power consumption and price are converted into PPA. What is a PPA? In fact, it is performance, power consumption and area. Among them, performance has two meanings. In front-end design, it refers to how many standard test program points can be run per Hertz. When designing a processor, there will be an idea of how many levels of pipelines there are. Generally speaking, the more pipeline stages there are, the higher the maximum frequency that the chip can run. Everyone should know this. But it does not mean that the higher the frequency, the higher the performance. This has a lot to do with the processor architecture. A typical counterexample is Intel's Pentium 4, which has more than 30 stages of pipeline and a maximum frequency of up to 3G Hz. However, because the pipeline is too long, once the instruction prediction is wrong, the re-fetched instructions will have to re-run the dozens of stages of the pipeline, which is very costly. . And its instruction prediction relied heavily on the compiler for optimization. As a result, the compiler failed to keep up, resulting in low overall performance. You see, the processor frequency of MIPS or PowerPC is not high, but the performance per Hertz is relatively good, and the overall performance will be improved. So when looking at performance, look at the overall running score, not the running score per Hz. Loongson took advantage of this loophole in its promotion some time ago, claiming that it can catch up with the Xeon per Hertz, but it can only run more than 1Ghz, while the 16-core Xeon can reach nearly 3Ghz.
Another meaning of performance is frequency, which is from the perspective of back-end design. Usually the people on the back end don't care about how many scores they can achieve per Hz, they only care about how many frequencies the chip can run at. The higher the frequency, the higher the overall performance given a certain number of points per Hz. Please note that for those programs running on the L1 cache, the processor score per Hz does not change with frequency. Of course, if you take into account multi-level caches, buses and peripheral interfaces, it certainly does not increase linearly with frequency. I will slowly expand on system-level performance issues in the future.
So what factors will affect frequency? Even if you only consider it from the back-end perspective, there are many answers. I am not in the back-end and manufacturing process, so I can only write down what I hear from hearsay, for reference only.
First, it is affected by the craftsmanship. There are only a few advanced semiconductor factories now, including Intel, TSMC, Samsung, UMC, and GlobalFoundries. Take TSMC as an example. It currently provides a 16-nanometer process, which is divided into many small nodes, such as FFLL++ and FFC. Each small node has its own characteristics, some can run to higher frequencies, some have low power consumption, and some have low cost. On different processes, the maximum frequency that the chip can run is different, and the power consumption and area are also different.
Secondly, it is affected by the back-end library. TSMC will abstract the parameters of the transistors in the process and create a physical layer development kit PDK, which will be provided to EDA tool manufacturers, IP manufacturers and chip manufacturers. The back-end engineers of these manufacturers will use this physical layer development kit to build their own physical libraries. The physical library generally contains two major parts: logic and memory. Depending on the channel length of the transistor, the unit cell will have different characteristics and be suitable for different purposes. How to rationally use cells in these libraries with different characteristics into different front-end design modules is a big question. Generally speaking, the shorter the channel length, the shorter the electron drift distance and the higher the frequency it can run. However, the higher the frequency, the greater the power consumption, and it increases exponentially. In addition to cell, there is also the term 9T/12T. The T here is Track, which is the height of the cell. The larger T, the larger the current, the easier it is to achieve high frequency, and the corresponding area is larger. Another adjustable parameter is the Voltage Threshold, which determines the voltage threshold of the gate. The lower the threshold, the greater the leakage, and the higher the frequency can rise .
Next, there are the influences of layout and routing. The inside of the chip also needs wiring, just like the motherboard. Each layer has a utilization rate. The smaller the overall area, the higher the utilization rate, but the more difficult it is to wire. After giving some initial and limiting conditions, the EDA software will continue to calculate by itself , and finally give a feasible frequency and area.
Again, influenced by front-end and back-end collaborative design. For example, for an operation that accesses memory, if you know how much time the processor will spend and what resources are used, you can close the free block of memory to save power. There may be thousands of ways to do this, and you won't know it unless you design the processor yourself, even if you have RTL code.
Further up, there is dynamic voltage frequency scaling DVFS. The concept of power consumption needs to be introduced here. Chip power consumption is divided into two parts: dynamic and static. Static is caused by transistor leakage, and its size is related to the chip process, number of transistors, and voltage. Dynamic is caused by switching, so it is related to the number of transistors, frequency, and voltage. I won’t list the specific formulas, they are available online. The method to control dynamic power consumption is clockgating. As the frequency becomes smaller, the dynamic power consumption will naturally decrease. The method to control static power consumption is power gating. Turn off the power, then both static and dynamic power consumption will be gone. The voltage can also be reduced, so the dynamic power consumption and static power consumption will naturally be small. However, the voltage cannot be reduced infinitely, otherwise the electrons will not be able to drift and the transistor will not work. Moreover, transistors running at different frequencies require different voltages. Taking 16nm as an example, it can change from 0.9V to 0.72V, and it can become 1V or higher. Don't underestimate this small voltage change. You must know that the change in dynamic power consumption is related to the voltage to the power of 2-3. 1V and 0.7V, the voltage difference is 50%, and the dynamic power consumption can be 3.4 times different. The data I have seen shows that below 500Mhz, the dynamic power consumption of the processor is less than the static power consumption. When it reaches 3GHz, it is much higher than the static power consumption.
再往上,就是软件电源管理,控制功耗了。芯片设计者把每个大模块的clock gating和power gating进行组合,形成不同的休眠状态,软件可以根据温度和运行的任务,动态的告诉处理器每个模块进入不同的休眠状态,从而在任务不忙的时候降低功耗。这又是一个很大的话题,以后再展开。
从上面我们可以看到,功耗和性能其实是 合 在一起的。 而芯片设计者可以用不同的工艺和物理库,设计出最高可运行频率,然后软件控制芯片动态运行频率和功耗。
那面积呢?其实也是相辅相成的。由于针对不同的逻辑,memory和布线,选用了不同的物理库cell,不同的track,形成的芯片面积也会不一样。通常来说,越是需要跑高频的芯片,所需的面积越大。频率差一倍,面积可能有百分之几十的差别。可别小看这百分之几十,对晶体管来说,面积就是成本,晶圆的总面积一定,价钱一定,那单颗芯片的面积越小,成本越低,并且此时良率也越高。
芯片成本除了制造费,还来自于授权费,工具费,流片费,运营开销等,通常手机处理器这样复杂的芯片,没有上千万美元是不可能做出来的。就算做出来,没有卖掉几百万片,那是肯定亏的。
最后还想提下ARM的大小核设计。其最初的目的是想设计两组核,小核每赫兹性能低,面积小,跑在低频;大核每赫兹性能高,面积大,跑在高频。运行简单任务,大核关闭,小核在低频,动态功耗低,静态功耗占上风,并且由于面积小,总体功耗更低。而大核用高频运行复杂任务。和x86的单纯调节电压频率比,增加了一点低频小核面积,和整个芯片的面积比,其实没多多少。
那为什么不让小核跑在高频运行复杂任务呢?理论上,由于每赫兹性能低,对于相同的任务,小核必须跑在比大核更高的频率才能完成,这就意味着更高的电压。此时,动态功耗占上风,并且和电压成三次方关系。最终的功耗会高出大核不少。此外,我们前面已经解释过,小核要跑在高频,面积会增大不少,可能比大核还要大。我们从里面可以看到存在一个平衡点。这个平衡点并不好找。拿A53/A57在28nm上举例,当它们跑在1.2Ghz的时候,功耗可能差两倍,性能却只差50%。而平衡点可能要达到2Ghz。事实上,很多手机芯片的大小核都是使用同样的处理器,跑在不同高低频率。
所以,设计芯片很大程度上就是在平衡。影响因素,或者说坑,来自于方方面面,IP提供商,工厂,市场定义,工程团队。水很深,坑很大,没有完美的芯片,只有完美的平衡。在这点上,苹果是一个很典型的例子。它的CPU频率不很高,但是Geekbench单核跑分却比海思的 A73高了整整75%,接近Intel桌面处理器的性能。为什么?一个原因是它使用了六发射,而A73只有双发射,流水线宽了整整三倍。这里,苹果用了大量的面积换取性能。当然,三倍的发射宽度并不表示性能就是三倍,由于数据相关性的存在,发射宽度的效益是递减的。再一点,苹果使用了整整6MB的缓存,而这个数字在别的手机芯片上通常是1MB。我做过测试,对一些标准跑分,比如SpecInt2000/2006,128KB到256KB二级缓存带来的性能提升仅仅是7%左右,而256KB到1MB带来的提升更小,缓存面积却是4倍。面积的提升同样带来了静态功耗的增加。不过由于苹果的生态都是他自己的,它引入的复杂的电源,电压和时钟控制,并从软件层面就开始优化,将整体功耗控制的非常好。但是也只有苹果能这么做,一般公司绝对不会走苹果这样用2-3倍面积换性能和功耗的路线,那样的话毛利就太低了。而没有手机整体高利润的保障,也没有统一的软件系统控制功耗,其结果就是一个死。
下面,让我们从访存这个简单的问题开始讨论SoC。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的好处在这里就体现出来了。当然,这也是有代价的,存储未完成的读请求的状态需要额外的缓冲,而处理器可能也需要支持乱序执行,造成面积和功耗进一步上升。对于写指令,只要storebuffer没满,还是一个时钟周期。当然,如果流水线上某个节拍大于一个时钟周期,那平均的延时就会取决于这个最慢的时间。在读取二级,三级缓存和内存的时候,我们可以把等待返回看作一个节拍,那么就能很自然的理解此时的延迟了。由此,我们可以得到每一级缓存的延迟和访存延迟。
上图画了读写指令经过的单元。我把流程简单描述下:
当写指令从存取单元LSU出发,它首先经过一个小的store queue,然后进入store buffer。之后,写指令就可以完成了,处理器不必等待。Storebuffer通常由几个8-16字节的槽位组成,它会对自己收到的每项数据进行地址检查,如果可以合并就合并,然后发送请求到右边的一级缓存,要求分配一行缓存,来存放数据,直到收到响应,这称作写分配writeallocate。当然,等待的过程可以继续合并同缓存行数据。如果数据是Non-Cacheable的,那么它会计算一个等待时间,然后把数据合并,发送到总线接口单元BIU里面的写缓冲Writebuffer。而写缓冲在把数据发到二级缓存之前,会经过监听控制单元,把四个核的缓存做一致性。过程和总线描述的类似,就不多讲了。
当读指令从存取单元LSU出发,无论是否Cacheable的,都会经过一级缓存。如果命中,那么直接返回数据,读指令完成。如果未命中,那么Non-Cacheable的请求直接被送到Read Buffer。如果是Cacheable的,那么一级缓存需要分配一个缓存行,并且把原来的数据写出到替换缓冲eviction buffer,同时发起一个缓存行填充,发送到Linefill Buffer。evictionbuffer会把它的写出请求送到BIU里面的Writebuffer,和Store Buffer送过来的数据一起,发到下一级接口。然后这些请求又经过监听控制单元做一致性检测后,发到二级缓存。当然有可能读取的数据存在于别的处理器一级缓存,那么就直接从那里抓取。
过程并不复杂,但程序员关心的是这个过程的瓶颈在哪,对读写性能影响如何。我们已经解释过,对于写,由于它可以立刻完成,所以它的瓶颈并不来自于存取单元;对于读,由于处理器会等待,所以我们需要找到读取路径每一步能发出多少OT,每个OT的数据长度是多少。
拿Cortex-A7来举例,它有2x32字节linefill buffer,支持有条件的miss-under-miss(相邻读指令必须在3时钟周期内),也就是OT最多等于2,而它的数据缓存行长度是64字节,所以每个OT都是半个缓存行长度。对于Cacheable的读来说,我还关心两个数据,就是evictionbuffer和Write buffer,它们总是伴随着linefill。在A7中,存在一个64字节的eviction buffer和一个Write buffer。有了这些条件,那么我就可以说,对于连续的读指令,我能做到的OT就是2,而linefill的速度和eviction,write buffer的速度一致,因为2x32=64字节。
那这个结论是不是正确?写个小程序测试下就知道。我们可以关掉二级缓存,保留一级缓存,然后用以下指令去读取一个较大的内存区域。所有的地址都是缓存行对齐,对齐的意义我就不说了,不对齐,甚至越过缓存行边界,会把一个操作变成两个,肯定会慢。伪代码如下:
loop
load R0, addr+0
load R0, addr+4
load R0, addr+8
load R0, addr+12
addr=addr+16
这里通过读取指令不断地去读数据。通过处理器自带的性能计数器看了下一级缓存的未命中率,6%多一点。这恰恰是4/64字节的比率。说明对于一个新的缓存行,第一个四字节总是未命中,而后面15个四字节总是命中。当然,具体的延迟和带宽还和总线,内存控制器有关,这里只能通过命中率简单验证下。
对于有的处理器,是严格顺序执行的,没有A7那样的miss-under-miss机制,所以OT=1。我在Cortex-R5上做同样的实验,它的缓存行长度是32字节,2xLinefillbuffer是32字节。测试得到的命中率是12%多点。也完全符合估算。
但是为什么R5要设计两个32字节长度的Linefillbuffer?既然它的OT=1,多出来的一个岂不是没用?实际上它是可以被用到的,而方法就是使用预取指令PLD。预取指令的特点就是,它被执行后,处理器同样不必等待,而这个读请求会被同样发送到一级缓存。等到下次有读指令来真正读取同样的缓存行,那么就可能发现数据已经在那了。它的地址必须是缓存行对齐。这样,读也可像写那样把第二个Linefill buffer给用上了。
我们把它用到前面的例子里:
loop
PLD addr+32
load 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可以更大,那就是缓存的硬件预取。当程序访问连续的或者有规律的地址时,缓存会自动检测出这种规律,并且预先去把数据取来。这种方法同样不占用处理器时间,但是也会占用linefillbuffer,eviction buffer和writebuffer。所以,如果这个规律找 得 不好,那么反而会降低效率。
读看完了,那写呢?Cacheable的写,如果未命中缓存,就会引发writeallocate,继而造成Linefill和eviction,也就是读操作。这点可能很多程序员没想到。当存在连续地址的写时,就会伴随着一连串的缓存行读操作。有些时候,这些读是没有意义的。比如在memset函数中,可以直接把数据写到下一级缓存或者内存,不需要额外的读。于是,大部分的ARM处理器都实现了一个机制,当探测到连续地址的写,就不让storebuffer把数据发往一级缓存,而是直接到write buffer。并且,这个时候,更容易合并,形成突发写,提高效率。在Cortex-A7上它被称作Read allocate模式,意思是取消了writeallocate。而在有的处理器上被称作streaming模式。很多跑分测试都会触发这个模式,因此能在跑分上更有优势。
但是,进入了streaming模式并不意味着内存控制器收到的地址都是连续的。想象一下,我们在测memcpy的时候,首先要从源地址读数据,发出去的是连续地址,并且是基于缓存行的。过了一段时间后,缓存都被用完,那么eviction出现了,并且它是随机或者伪随机的,写出去的地址并无规律。这就打断了原本的连续的 读地址。 再看写,在把数据写到目的地址时,如果连续的写地址被发现,那么它就不会触发额外的linefill和eviction。 这是好事。 可是,直接写到下一级缓存或者内存的数据,很有可能并不是完整的缓存发突发写, 应 为storebuffer也是在不断和write buffer交互的,而writebuffer还要同时接受eviction buffer的请求。 其结果就是写被分成几个小段。 这些小块的写地址,eviction的写地址,混合着读地址,让总线和内存控制器增加了负担。 它们必须采用合适的算法和参数,才能合并这些数据,更快的写到内存颗粒。
然而事情还没有完。我们刚才提到,streaming模式是被触发的,同样的,它也可以退出。退出条件一般是发现存在非缓存行突发的写。这个可能受writebuffer的响应时间影响。退出后,write allocate就又恢复了,从而读写地址更加不连续,内存控制器更加难以优化,延时进一步增加,反馈到处理器,就更难保持在streaming模式。
再进一步,streaming模式其实存在一个问题,那就是它把数据写到了下一级缓存或者内存,万一这个数据马上就会被使用呢?那岂不是还得去抓取?针对这个问题,在ARMv8指令集中(适用于A53/57/72),又引入了新的一条缓存操作指令DCZVA,可以把整行缓存设成0,并且不引发write allocate。为什么?因为整行数据都被要改了,而不是某个字段被改,那就没有必要去把原来的值读出来,所以只需要allocate,不需要读取,但它还是会引发eviction。类似的,我们也可以在使用某块缓存前把它们整体清除并无效化,clean&invalidate,这样就不会有eviction。不过如果测试数据块足够大,这样只是相当于提前做了eviction,并不能消除,让写集中在某段。使之后的读更连续。
以上都是针对一级缓存。二级缓存的控制力度就小些,代码上无法影响,只能通过设置寄存器,打开二级缓存预取或者设置预取偏移。我在ARM的二级缓存控制器PL301上看到的,如果偏移设置 得 好,抓到的数据正好被用上,可以在代码和一级缓存优化完成的基础上,读带宽再提升150%。 在新的处理器上,同时可以有多路的预取,探测多组 访 存模板,进一步提高效率。 并且,每一级缓存后面挂的OT数目肯定大于上一级,它包含了各类读写和缓存操作,利用好这些OT,就能提高性能。
对于Non-Cacheable的写,它会被storebuffer直接送到write buffer进行合并,然后到下一级缓存。对于Non-Cacheable的读,我们说过它会先到缓存看看是不是命中,未命中的话直接到read buffer,合并后发往下一级缓存。它通常不占用linefillbuffer,因为它通常是4到8字节,不需要使用缓存行大小的缓冲。
我们有时候也可以利用Non-Cacheable的读通道,和Cacheable的读操作并行,提高效率。它的原理就是同时利用linefill buffer和readbuffer。此时必须保证处理器有足够的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处理器周期,已经慢于至强了。
如果上面一段看完你没什么感觉,那我可以换个说法:对于没有完整支持stashing的ARM SoC,哪怕处理器跑在10Ghz,网络加速器性能强的翻天,基于DPDK的简单包转发(快于Linux内核网络协议栈转发几十倍)还是只能到zhi强的30%,而包转发是网络处理器的最重要的指标之一,也是服务器用跑网络转发软件的指标之一,更可以用在存储领域,加速SPDK之类的存储应用。
还有,在ARM新的面向网络和服务器的核心上,会出现一核两线程的设计。处理包的任务天然适合多线程,而一核两线程可以更有效 地 利用硬件资源,再加上stashing,如虎添翼。
弄清了访存的路径,可能就会想到一个问题:处理器发出去的读写请求到底是个什么东西?要想搞清楚它,就需要引入总线。下文我拿ARM的AXI/ACE总线协议以及由它衍生的总线结构来展开讨论。这两个协议广泛用于主流的手机芯片上,是第四代AMBA(Advanced Microcontroller Bus Architecture)标准。
简单的总线就是一些地址线和数据线,再加一个仲裁器,就可以把处理器发过来的读写请求送到内存或者外设,再返回数据。在这个过程中,我们需要一个主设备,一个从设备,所有的传输都是主设备发起,从设备回应。让我们把处理器和它包含的缓存看作一个主设备,把内存控制器看作从设备。处理器发起访问请求,如果是读,那么总线把这个请求(包括地址)送到内存控制器,然后等待回应。过了一段时间,内存控制器把内存颗粒里面读出的数据交给总线,总线又把数据交给处理器。如果数据无误(ECC或者奇偶校验不出错),那么这个读操作就完成了。如果是写,处理器把写请求(包括地址)和数据交给总线,总线传递给内存控制器,内存控制器写完后,给出一个确认。这个确认经由总线又回到了处理器,写操作完成。
以上过程有几个重点。第一,处理器中的单个读指令,被分为了请求(地址),完成(数据)阶段。写指令也被分为了请求(地址,数据),完成(写入确认)阶段。第二,作为从设备,内存控制器永远都无法主动发起读写操作。如果一定要和处理器通讯,比如发生了读写错误,那就得使用中断,然后让处理器来发起读写内存控制器状态的请求。第三,未完成的读写指令就变成了OT,总线可以支持多个OT。然而,总线支持多OT并不表示处理器能发送这么多请求出来,尤其是读。所以瓶颈可能还是在处理器。
我遇到过几次这样的情况,在跑某个驱动的时候,突然系统挂死。但是别的设备中断还能响应,或者报个异常后系统又继续跑了。如果我们把上文的内存控制器替换成设备控制器,那就不难理解这个现象了。假设处理器对设备发起读请求,而设备没有回应,那处理器就会停在那等待。我看到的处理器,包括PowerPC, ARM,都没有针对这类情况的超时机制。如果没有中断,那处理器无法自己切换到别的线程(Linux等操作系统的独占模式),就会一直等待下去,系统看上去就挂住了。有些设备控制器可以自动探测这类超时,并通过中断调用相应的异常或者中断处理。在中断处理程序中,可以报个错,修改返回地址,跳过刚才的指令往下走,系统就恢复了。也有些处理器在触发某类异常后能自动跳到下一行指令,避免挂死。但是如果没有异常或者中断发生,那就永远挂在那。
继续回到总线。在AXI/ACE总线协议中,读和写是分开的通道,因为他们之间并没有必然联系。更细一些,总线上规定了五个组,分别是读操作地址(主到从),读操作数据(从到主),写操作地址(主到从),写操作数据(主到从),写操作确认(从到主)。读和写两大类操作之间,并没有规定先后次序。而在每一类操作之内的组之间,是有先后次序的,比如地址是最先发出的,数据随后,可以有很多拍,形成突发操作。而确认是在写操作中,从设备收到数据之后给出的。对内存控制器,必须在数据最终写入到颗粒之后再给确认,而不是收到数据放到内部缓存中的时候。
对于同一个通道,如果收到连续的指令,他们之间的次序是怎么样的呢?AXI/ACE协议规定,次序可以打乱。拿读来举例,前后两条读指令的数据返回是可以乱序的。这里包含了一个问题,总线怎么区分住前后两读条指令?很简单,在地址和数据组里加几根信号,作为标志符,来区分0-N号读请求和完成。每一对请求和完成使用相同的标志符。有了这个标志符,就不必等前一个请求完成后才开始第二个请求,而是让他们交替进行,这样就可以实现总线的OT,极大提高效率。当然,也需要提供相应的缓冲来存储这些请求的状态。并且最大的OT数取决于缓冲数和标志符中小的那个。原因很简单,万一缓冲或者标志符用完了,但是所有的读操作全都是请求,没有一个能完成怎么办?那只好让新的请求等着了。于是就有了AXI/ACE总线的一条规则,同一个读或者写通道中,相同标志符的请求必须按顺序完成。
有时候,处理器也会拿这个标志符作为它内部的读写请求标志符,比如Cortex-A7就是这么干的。这样并不好,因为这就等于给自己加了限制,最大发出的OT不得大于总线的每通道标志符数。当一个处理器组里有四个核的时候,很可能就不够用了,人为限制了OT数。
最后,读写通道之间是没有规定次序的,哪怕标志相同。
看到这里可能会产生一个问题,读写指令里面有一个默认原则,就是相同地址,或者地址有重叠的时候,访存必须是顺序的。还有,如果访问的内存类型是设备,那么必须保证访存次序和指令一致。这个怎么在总线上体现出来呢?总线会检查地址来保证次序,一般是内存访问前后乱序地址不能64字节内,设备访问前后乱序地址不能在4KB内。
在AXI/ACE中,读和写通道的比例是一比一。实际上,在日常程序中,读的概率比写要大。当然,写缓存实际上伴随着缓存行填充linefill(读),而读缓存会造成缓存行移除eviction(写),再加上合并和次序调整,所以并不一定就是读写指令的比例。我看到FreescalePowerPC的总线CCB,读写通道的比率是二比一。我不知道为什么ARM并没有做类似的设计来提高效率,也许一比一也是基于手机典型应用统计所得出的最好比例。
至此,我们已经能够在脑海中想象一对读写通道中读写操作的传输情况了。那多个主从设备组合起来是怎么样的情况?是不是简单 的 叠加? 这涉及到了总线设计最核心的问题,拓扑结构。
CCN总线上的每一个节点,除了可以和相邻的两个节点通讯之外,还可以附加两个节点组件,比如处理器组,三级缓存,内存控制器等。在节点内部,还是交叉的,而在节点之间,是环状的。这样使得总线频率在某种程度上摆脱了连接设备数量的限制(当然,还是受布线等因素的影响),在16纳米下,可以达到1.2GHz以上。当然,代价就是节点间通讯更大的平均延迟。为了减少平均延迟,可以把经常互相访问的节点放在靠近的位置。
在有些系统里,要求连接更多的设备,并且,频率要求更高。此时环状总线也不够用了,这时需要网状总线CMN。ARM的网状总线,符合AMBA5.0的CHI接口,支持原子操作(直接在缓存运算,不用读取到处理器),stashing和直接访问(跳过中间的缓存,缩短路径)等特性,适用于服务器或者网络处理器。
这个图中,刚才提到的交叉矩阵,可以作为整个网络的某部分。而连接整个系统的,是位于NoC内的节点。每个节点都是一个小型路由,它们之间传输的,是异步的包。这样,就不必维持路由和路由之间很大数量的连线,从而提高频率,也能支持更多的设备。当然,坏处就是更长的延迟。根据我看到的数据,在16纳米上,频率可以跑到1.5Ghz。并且它所连接每个子模块之间,频率和拓扑结构可以是不同的。可以把需要紧密联系的设备,比如CPU簇,GPU放在一个子网下减少通讯延迟。
不知你有没有发现,以上每一种总线,都可以在网络设备上找到原型。从设备直连,到环状局域网,到交换和路由。他们的拓扑结构是相通的。
在实际的ARM生态系统中,以上三种拓扑结构的使用情况是怎么样的呢?一般手机芯片上使用交叉矩阵,网络处理器和服务器上使用环状网络,而网状拓扑也被大量应用于手机芯片。最后一个的原因倒不是手机上需要连接的设备数太多,而是因为AXI/ACE协议都不支持一次传输拆分成多个,送给不同的内存控制器。这类传输有个名词,叫做interleaving,交叉访问。交叉访问有很多定义,从总线角度看,一种是一个主设备和一个从设备之间的多个读写请求之间的完成次序可以打乱,这个我们已经解释过。第二种是对于一个主设备的一次读写请求,把它分别送到多个从设备,并等待完成。为什么需要这种传输呢?因为在手机里面,GPU,显示控制器,视频控制器对内存带宽要求是很高的。一个1080p的屏幕,每秒要刷新60次,2百万个像素,每个像素32比特颜色,再加上4层图层,就需要2GB/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,但是还没什么实际应用。
多出来的监听通道,同样也有地址(从到主),回应(主到从)和数据(主到从)。每组信号内都包含和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),把它所有处理器组内部一级二级缓存的状态信息都放在里面。数据缓存(DATARAM)是不需要的,因为它只负责查看命中与否。这样做的好处就是,监听请求不必发到各组处理器,在总线内部就可以完成,省了将近10个总线周期,功耗也优于访问内存。它的代价是增加了一点缓存(一二级缓存的10%左右)。并且,如果监听过滤器里的某行缓存被替换(比如写监听命中,需要无效化(Invalidate)缓存行,MOESI协议定义),同样的操作必须在对应处理器组的一二级缓存也做一遍,以保持一致性。这个过程被称作反向无效化,它添加了额外的负担,因为在更新一二级缓存的时候,监听过滤器本身也需要追踪更新的状态,否则就无法保证一致性。幸好,在实际测试中发现,这样的操作并不频繁,一般不超过5%的可能性。当然,有些测试代码会频繁的触发这个操作,此时监听过滤器的缺点就显出来了。
在经过实际性能测试后,CCI设计人员发现总线瓶颈移到了访问这个监听过滤器的窗口,这个瓶颈其实掩盖了上文的反向无效化问题,它总是先于反向无效化被发现。把这个窗口加大后,又在做测试时发现,如果每个主从接口都拼命灌数据(主从设备都是OT无限大,并且一主多从有前后交叉),在主从设备接口处经常出现等待的情况,也就是说,明明数据已经准备好了,设备却来不及接收。于是,又增加了一些缓冲来存放这些数据。其代价是稍大的面积和功耗。请注意,这个缓冲和存放OT的状态缓冲并不重复。
根据实测数据,在做完所有改进后,新的总线带宽性能同频增加50%以上。而频率可以从500Mhz提高到1GMhz。当然这个结果只是一个模糊的统计,如果我们考虑处理器和内存控制器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总线中是不支持壁垒的,必须在芯片设计阶段,通过配置选项告诉处理器自己处理壁垒指令,不要送到总线。但这并不影响程序中的壁垒指令,处理器会在总线之前把它过滤掉。
此时,flag在Master0接口中等待它的所有下一级接口的壁垒响应。而data达到了Slave0后,壁垒响应走到了Master0接口,flag继续往下走。此时,我们不必担心data没有到slave0,因为那之前,来自Slave0接口的壁垒响应不会被送到Master0接口。这样,就做到了弱壁垒的次序保证,并且在壁垒指令完成前,flag的请求就可以被送出来。
对于强壁垒指令来说,仅仅有一个区别,就是Master0接口在收到所有下一级接口的壁垒响应前,它不会发送自身的壁垒响应给Master0。这就造成flag发不出来,直到壁垒指令完成。如下图:
这样,就保证了强壁垒完成后,下一条读写指令才能发出请求。此时,强壁垒前的读写指令肯定是完成了的。
另外需要特别注意的是,ARM的弱壁垒只是针对显式数据访问的次序。什么叫显式数据访问?读写指令,缓存,TLB操作都算。相对的,什么是隐式数据访问?在处理器那一节,我们提到,处理器会有推测执行,预先执行读写指令;缓存也有硬件预取机制,根据之前数据访问的规律,自动抓取可能用到的缓存行。这些都不包含在当前指令中,弱壁垒对他们无能为力。因此,切记,弱壁垒只能保证你给出的指令次序,并不能保证在它们之间没有别的模块去访问内存,哪怕这个模块来自于同一个核。
简单来说,如果只需要保证读写次序,用弱壁垒;如果需要某个读写指令完成才能做别的事情,用强壁垒。以上都是针对普通内存类型。当我们把类型设成设备时,自动保证强壁垒。
我们提到,壁垒只是针对单核。在多核多线程时,哪怕使用了壁垒指令,也没法保证读写的原子性。解决办法有两个,一个是软件锁,一个是原子操作。原子操作我看到过的有两种,一种是总线收到请求时,直接封掉整个总线,同时只有一个核能访问。这样效率很低。还有个方法是把锁的请求发送到对端设备,比如内存控制器,让他禁止别的核的访问,而总线依然可以运行,这样效率就高不少,我看到过的数据,减少10倍时间。但是AXI/ACE协议不支持原子操作。所以需要用到软件锁。
软件锁中有个自旋锁,能用一个ARM硬件机制exclusiveaccess来实现。当使用特殊指令对一个地址写入值,相应缓存行上会做一个特殊标记,表示还没有别的核去写这行缓存。然后下条指令读这个行,如果标记没变,说明写和读之间没有人打扰,那么就拿到锁了。如果变了,那么回到写的过程重新获取锁。由于缓存一致性,这个锁变量可以被多个核与线程使用。当然,过程中还是需要壁垒指令来保证次序。
对于普通内存,还会产生一个问题,就是读写操作可能会经过缓存,你不知道数据是否最终写到了内存中。通常我们使用clean操作来刷缓存。但是刷缓存本身是个模糊的概念,缓存存在多级,有些在处理器内,有些在总线之后,到底刷到哪里算是终结呢?还有,为了保证一致性,刷的时候是不是需要通知别的处理器和缓存?为了把这些问题规范化,ARM引入了Point of Unification/Coherency,Inner/OuterCacheable和System/Inner/Outer/Non Shareable的概念。
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。
Shareable的很容易理解,就是某个地址的可能被别人使用。我们在定义某个页属性的时候会给出。Non-Shareable就是只有自己使用。当然,定义成Non-Shareable不表示别人不可以用。某个地址A如果在核1上映射成Shareable,核2映射成Non-Shareable,并且两个核通过CCI400相连。那么核1在访问A的时候,总线会去监听核2,而核2访问A的时候,总线直接访问内存,不监听核1。显然这种做法是错误的。
对于Inner和Outer Shareable,有个简单的的理解,就是认为他们都是一个东西。在最近的ARM A系列处理器上上,配置处理器RTL的时候,会选择是不是把inner的传输送到ACE口上。当存在多个处理器簇或者需要双向一致性的GPU时,就需要设成送到ACE端口。这样,内部的操作,无论inner shareable还是outershareable,都会经由CCI广播到别的ACE口上。
说了这么多概念,你可能会想这有什么用处?回到上文的Clean指令,PoU使得四个A7的指令缓存中对应的行都被清掉。由于是指令缓存操作,InnerShareable属性使得这个操作被扩散到总线。而CCI400总线会把这个操作广播到所有可能接受的口上。ACE口首当其冲,所以四个A15也会清它们对应的指令缓存行。对于Mali和DMA控制器,他们是ACE-Lite,本不必清。但是请注意它们还连了DVM接口,专门负责收发缓存维护指令,所以它们的对应指令缓存行也会被清。不过事实上,它们没有对应的指令缓存,所以只是接受请求,并没有任何动作。
你可能又会想,我们要这么复杂的定义有什么用?用处是,精确定义缓存维护和读写指令的范围。如果我们改变一下,总线不支持Inner/Outer Shareable的广播,那么就只有A7处理器组会清缓存行。显然这么做在逻辑上不对,因为A7/A15可能运行同一行代码。并且,我们之前提到过,如果把读写属性设成Non-Shareable,那么总线就不会去监听其他主,减少访问延迟,这样可以非常灵活的提高性能。
再回到前面的问题,刷某行缓存的时候,怎么知道数据是否最终写到了内存中?对不起,非常抱歉,还是没法知道。你只能做到把范围设成PoC。如果PoC是三级缓存,那么最终刷到三级缓存,如果是内存,那就刷到内存。不过这在逻辑上没有错,按照定义,所有Master如果都在三级缓存统一数据的话,那就不必刷到内存了。
简而言之,PoU/PoC定义了指令和命令的所能抵达的缓存或内存,在到达了指定地点后,Inner/OuterShareable定义了它们被广播的范围。
再来看看Inner/Outer Cacheable,这个就简单了,仅仅是一个缓存的前后界定。一级缓存一定是InnerCacheable的,而最外层的缓存,比如三级,可能是Outer Cacheable,也可能是Inner Cacheable。他们的用处在于,在定义内存页属性的时候,可以在不同层的缓存上有不同的处理策略。
在ARM的处理器和总线手册中,还会出现几个PoS(Pointof Serialization)。它的意思是,在总线中,所有主设备来的各类请求,都必须由控制器检查地址和类型,如果存在竞争,那就会进行串行化。这个概念和其他几个没什么关系。
纵观整个总线的变化,还有一个核心问题并没有被提及,那就是动态规划re-scheduling与合并Merging。处理器和内存控制器中都有同样的模块,专门负责把所有的传输进行分类,合并,调整次序,甚至预测未来可能接收到的读写请求地址,以实现最大效率的传输。这个问题在分析性能时会重新提到。但是在总线这层,软件能起的影响很小。清楚了总线延迟和OT最大的好处是可以和性能计数器的统计结果精确匹配,看看是不是达到预期了。
关于NoC再补充一些:
NoC(Arteris,Netspeed, Sonics公司)早就大量用于手机和平板处理器了(MTK,Qualcom, Hisilicon, Spreadtrum, Samsung),它的主要用途是连接CPU和GPU以外的众多设备。好处如下:
跟ARM的基于crossbar的NIC系列总线比,它最大的好处在于可以连接多个内存控制器,并且可以支持内存控制器的interleaving和striping访问。为什么需要多个内存控制器?上了1080p以后,视频访问带宽成为了一个大瓶颈,必须使用两个甚至四个内存控制器。并且,需要总线把一次传输自动拆成几个,均匀发送到多个内存控制器,并自动合并数据,送回主设备。这当中还需要reorderbuffer来存储中间数据,提高效率。所有这些ARM NIC都不支持,所以连接多个内存控制器并交织访问的时候,是必须用NoC的。
NoC的另外一个特点是采用路由器结构进行包转发,可以减少管脚,提高时钟频率,16纳米上到1Ghz也不稀奇。并且支持多时钟域,不同的子网之间可以跑在不同频率,使用不同宽度,极其灵活。所以很多时候就算连慢速设备也是使用NoC。
但是如果系统需要支持的主设备不多,那其实还是使用NIC比较好,因为NoC的延迟相对较大,NIC这种crossbar结构一个周期可以传过去的东西,到了NoC就要吭哧吭哧传5-10个周期。当然,如果是视频流,由于流水线的存在,对于延迟并不敏感,只要带宽和优先级够了就行。
现在ARM推出了提供硬件一致性的网络结构CCI和CCN,前者还是crossbar,后者是环状和网状的。于是,对于有硬件一致性要求的场景,比如多处理器组,比如GPU和CPU要做异构计算,必须使用CCI/CCN。
和CCI相比,NoC的好处是支持很多个主设备,CCI由于受到crossbar结构的天然限制,最多做到7x5,再往下频率就上不去了(1Ghz@16nmFFC)。CCN弥补了CCI的限制,并且使用了网状结构后,频率也可以超过1Ghz,还能提供一致性。宽度也足够大,也支持多个内存控制器的interleaving和striping。所以成为了NoC的劲敌。
NoC当然也不是吃白饭的,它也可以提供一致性支持,但是延迟就是个大问题了。它天然的会使用更长的延迟,对于一致性检查这种靠广播的传输非常致命。如果处理器支持很多的并发访存,那还可以通过减少平均延迟来减缓下,但是如果数量少,那处理器只能每次传输都在那干等,效率很低。当然,也可以通过在某个总线节点插入额外的Tagram,保存缓存的标志位来解决这个问题,那又是另外的故事了。
其他的一些细节,比如支持非对齐访问,interleaving颗粒度更小,支持传输ID缩减,灵活的配置re-order buffer,支持更多的接口,只读/只写通道等等,都会影响系统的设计。这些就需要NoC公司和SoC厂家细细分析了。
总之,现在手机和平板上最常见的用法,CCI连接CPU和GPU,作为子网,网内有硬件一致性。NoC连接子网,同时连接其余的设备,包括多个内存控制器和视频,显示控制器,不需要一致性。优点是兼顾一致性,大带宽和灵活性,缺点是CPU/GPU到内存控制器要跨过两个网,延迟有点大。
访存路径的最后一步是内存。有的程序员认为内存是一个所有地址访问时间相等的设备,是这样的么?这要看情况。
DDR地址有三个部分组成,行,bank,列。一旦这三个部分定了,那么就可以选中确定的一个物理页,通常有2-8KB大小。我们买内存的时候,有3个性能参数,比如10-10-10。这个表示访问一个地址所需要的三个操作时间,行有效(包括选bank),列选通(命令/数据访问),还有预充电。前两个好理解,第三个的意思是,某个内存物理页暂时用不着,必须关闭,保持电容电压,否则再次使用这页数据就丢失了。如果连续的内存访问都是在同行同bank,那么第一和第三个10都可以省略,每一次访问只需要10单位时间;同行不同bank,表示需要打开一个新的页,只有第三个10可以省略,共20单位时间;不同行同bank,那么需要关闭老页面,打开一个新页面,预充电没法省,共30单位时间。
我们得到什么结论?如果控制好物理地址,就能使某段时间内的访存都集中在一个页内,从而节省大量的时间。根据经验,在突发访问时,最多可以省50%。那怎么做到这一点?去查查芯片手册中物理内存地址到内存管脚的映射,就可以得到需要的物理地址。然后调用系统函数,为这个物理地址分配虚拟地址,就可以使得程序只访问某个固定的物理内存页。
在访问有些数据结构时,特定的大小和偏移有可能会不小心触发不同行同bank这个条件。这样可能每次访问都是最差情况。为了避免这种最差情况的产生,有些内存控制器可以自动让最终地址哈希化,打乱原有的不同行同bank条件,从而在一定程度上减少延迟。我们也可以通过计算和调整软件物理地址来避免上述情况的发生。
我们可以通过突发访问,让上图中的绿色数据块更长,那么相应的利用率就越高。此时甚至不需要用到四个bank,如下图:
如果做的更好些,我们可以通过软件控制地址,让上图中的预充电,甚至行有效尽量减少,那么就可以达到更高的效率。还有,使用更好的内存颗粒,调整配置参数,减少行有效,列选通,还有预充电的时间,提高DDR传输频率,也是好办法,这点PC机超频玩家应该有体会。此外,在DDR板级布线的时候,控制每组时钟,控制线,数据线之间的长度差,调整好走线阻抗,做好自校准,设置合理的内存控制器参数,调好眼图,都有助于提高信号质量,从而可以使用更短的时序参数。
如果列出所有数据突发长度情况,我们就得到了下图:
上面这个图包含了更直观的信息。它模拟内存控制器连续不断的向内存颗粒发起访问。X轴表示在访问某个内存物理页的时候,连续地址的大小。这里有个默认的前提,这块地址是和内存物理页对齐的。Y轴表示同时打开了多少个页。Z轴表示内存控制器访问内存颗粒时带宽的利用率。我们可以看到,有三个波峰,其中一个在128字节,利用率80%。而100%的情况下,访问长度分别为192字节和256字节。这个大小恰恰是64字节缓存行的整数倍,意味着我们可以利用三个或者四个8拍的突发访问完成。此时,我们需要至少4个页被打开。
还有一个重要的信息,就是X轴和Z轴的斜率。它对应了DDR时序参数中的tFAW,限定单位时间内同时进行的页访问数量。这个数字越小,性能可能越低,但是同样的功耗就越低。
对于不同的DDR,上面的模型会不断变化。而设计DDR控制器的目的,就是让利用率尽量保持在100%。要做到这点,需要不断的把收到的读写请求分类,合并,调整次序。而从软件角度,产生更多的缓存行对齐的读写,保持地址连续,尽量命中已打开页,减少行地址和bank地址切换,都是减少内存访问延迟的方法。
交替访问也能提高访存性能。上文已经提到了物理页的交替,还可以有片选信号的交替访问。当有两个内存控制器的时候,控制器之间还可以交替。无论哪种交替访问,都是在前一个访问完成前,同时开始下一个传输。当然,前提必须是他们使用的硬件不冲突。物理页,片选,控制器符合这一个要求。交替访问之后,原本连续分布在一个控制器的地址被分散到几个不同的控制器。最终期望的效果如下图:
这种方法对连续的地址访问效果最好。但是实际的访存并没有上图那么理想,因为哪怕是连续的读,由于缓存中存在替换eviction和硬件预取,最终送出的连续地址序列会插入扰动,而如果取消缓存直接访存,可能又没法利用到硬件的预取机制和额外的OT资源(这两点在处理器那节有解释),从而使得分布不那么均匀。实测下来,可能会提升30%左右。此外,由于多个主设备的存在,每一个主都产生不同的连续地址,使得效果进一步降低。因此,只有采用总线那节所说的拆分访问才能真正的实现均匀访问多个内存控制器。当然,此时的突发长度和粒度要匹配,不然粒度太大也没法均匀。并且,就算均匀了也未必是最优的。对于某个内存控制来说,最好的期望是总收到同一个物理页内的请求。我看到的实际场景下,是对不同地址区域做不同颗粒大小和不同内存控制器的交织。比如视频处理器来的地址,拆成64字节小块,然后在1,2号内存控制器交织。处理器来的地址,不用交织,不用拆分,全都访问0号内存控制器,因为处理器的地址相对更随机,很容易打断视频的连续地址,所以他们之间不要交织,各自访问各自的内存控制器。
还有一点需要提及。如果使用了带ecc的内存,那么最好所有的访问都是ddr带宽对齐(一般64位)。因为使能ecc后,所有内存访问都是带宽对齐的,不然ecc没法算。如果你写入小于带宽的数据,内存控制器需要知道原来的数据是多少,于是就去读,然后改动其中一部分,再计算新的ecc值,再写入。这样就多了一个读的过程。根据经验,如果访存很多,关闭ecc会快8%。
下面是几年以前写的优化,一并贴出。
面向处理器结构的优化可以从以下几个方向入手:缓存命中,指令预测,数据预取,数据对齐,内存拷贝优化,ddr访问延迟,硬件内存管理优化,指令优化,编译器优化等级以及性能描述工具。
缓存未命中是处理器的主要性能瓶颈之一。在FSL的powerpc上,访问一级缓存是3个时钟周期,二级是12个,3级30多个,内存100个以上。一级缓存和内存访问速度差30多倍。我们可以算一下,如果只有一级缓存和内存,100条存取指令,100%命中和95%命中,前者300周期,后者95*3+5*100=785周期,差了1.6倍。这个结果的前提是powerpc上每个核心只有1个存取单元,使得多发射也无法让存取指令更快完成。当然,如果未命中的指令分布的好,当中穿插了很多别的非存取指令那就可以利用乱序多做些事情,提高效率。
我们可以用指令预测和数据预取。
指令预测很常见,处理器预测将要执行的一个分支,把后续指令取出来先执行。等真正确定判断条件的时候,如果预测对了,提交结果,如果不对,丢掉预先执行的结果,重新抓取指令。此时,结果还是正确的,但是性能会损失。指令预测是为了减少流水线空泡,不预测或者预测错需要排空流水线并重新从正确指令地址取指令,这个代价(penalty)对流水线深度越深的处理器影响越大,严重影响处理器性能。
指令预测一般是有以下几种办法:分支预测器(branch predictor)+btb+ras(Return Address Stack)+loop buffer。根据处理器类型和等级不同从以上几种组合。btb的话主要是为了在指令译码前就能预测一把指令跳转地址,所以btb主要是针对跳转地址固定的分支指令做优化(比如jump到一个固定地址),目的也是为了减少空泡。否则正常情况下即使预测一条分支跳转,也要等到译码后才能知道它是一条分支指令,进而根据branch predictor的预测结果发起预测的取指。而btb可以在译码前就通过对比pc发起取指。这样对每一条命中btb的分支指令一般可以省好几个时钟周期。大致方法是,对于跳转指令,把它最近几次的跳转结果记录下来,作为下一次此处程序分支预测的依据。举个例子,for循环1000次,从第二次开始到999次,每次都预取前一次的跳转地址,那么预测准确率接近99.9%。这是好的情况。不好的情况,在for循环里面,有个if(a[i])。假设这个a[i]是个0,1,0,1序列,这样每次if的预测都会错误,预取效率就很低了。改进方法是,把if拆开成两个,一个专门判断奇数次a[i],一个判断偶数次,整体循环次数减少一半,每次循环的判断增加一倍,这样每次都是正确的。如果这个序列的数字预先不可见,只能知道0多或者1多,那么可以用c语言里面的LIKELY/UNLIKELY修饰判断条件,也能提高准确率。需要注意的是,btb表项是会用完的,也就是说,如果程序太久没有走到上次的记录点,那么记录就会被清掉,下次再跑到这就得重新记录了。分支预测有个有趣的效应,如果一段代码处于某个永远不被触发的判断分支中,它仍然可能影响处理器的分支预测,从而影响总体性能。如果你删掉它,说不定会发现程序奇迹般的更快了。
数据预取,和指令预测类似,也是处理器把可能会用到的数据先拿到缓存,之后就不必去读内存了。它又分为软件预取和硬件预取两种,硬件的是处理器自己有个算法去预测抓哪里的数据,比如在访问同一类型数据结构的某个元素,处理器会自动预取下一个偏移的数据。当然,具体算法不会这么简单。软件预取就是用编译器的预编译宏修饰某个将要用到的变量,生成相应指令,手工去内存抓某个程序员认为快要用到的数据。为什么要提前?假设抓了之后,在真正用到数据前,有100条指令,就可以先执行那些指令,同时数据取到了缓存,省了不少时间。
需要注意的是,如果不是计算密集型的代码,不会跑了100个周期才有下一条存取指令。更有可能10条指令就有一次访存。如果全都未命中,那么这个预取效果就会打不少折扣。并且,同时不宜预取过多数据,因为取进来的是一个缓存行,如果取得过多,会把本来有用的局部数据替换出去。按照经验同时一般不要超过4条预取。此外,预取指令本身也要占用指令周期,过多的话,会增加每次循环执行时间。要知道有时候1%的时间都是要省的。
When accessing instructions or data, there is a very important matter, which is alignment. Four-byte alignment is not enough. It is best to use cache line alignment, which is generally used when doing memory copies, DMA or data structure assignments. When the processor reads the data structure, it is in row units, and the length can be 32 bytes or more. If the data structure can be adjusted to cache line alignment, then it can be read with the minimum number of times. In DMA, cache lines are generally used as units. If they are not aligned, there will be extra transmissions or even errors. Also, on the SoC system, when performing DMA on some device modules, if the cache lines are not aligned, then every 32 bytes may be split into 2 segments for DMA respectively, and the efficiency will be doubled.
If memory with ECC is used, DDR bandwidth alignment is even more necessary. Because after enabling ecc, all memory accesses are bandwidth aligned, otherwise ecc cannot be calculated. If you write data that is smaller than the bandwidth, the memory controller needs to know what the original data is, so it reads it, then changes part of it, calculates the new ecc value, and then writes it. This adds another reading process, which is much slower.
Another situation that requires alignment is data structure assignment. Suppose there is a 32-byte data structure filled with 4-byte elements. Normal initialization and clearing requires 32/4=8 assignments. There are some instructions that can directly set the cache line to all 0s or 1s. This way the time becomes 1/8. More importantly, a write cache miss actually requires reading the data from memory to the cache and then writing it. This means that write misses take the same amount of time as read misses. By using this instruction, the save instruction can directly write all 0/1 into the cache without reading the memory. This is logically no problem, because the data to be written (all 0/1) is already clear, and there is no need to read the memory. If this line is replaced in the future, the data is written back to memory. Of course, this instruction is also very restrictive. It must replace the entire cache line and cannot modify a single byte. This process is actually the optimized memset() function. If you adjust your big data structure, put all the elements that need to be cleared together in the same period, and then use optimized memset(), the efficiency will be much higher. Similarly, in the memcpy() function, since there are read source addresses and write destination addresses, as mentioned above, there may be two misses and two memory accesses are required. Now we can first write a cache line (no write miss), then read the source address and write the destination address, which becomes a total of 1 memory access operation. As for writing back data, that is something that the processor will do by itself later, so don't worry about it.
The memory operation functions in the standard libc library can be optimized in a similar way, not just four-byte alignment. However, it should be noted that if the given source and destination addresses are not cache line aligned, then the beginning and end data need additional processing, otherwise the entire line will be replaced, which will affect other data. In addition, prefetching can also be combined, taking out the first and last things to be used, and then doing a bunch of judgment logic, which can improve efficiency. However, if the tail is processed first, then when the memory overlaps, the source address content will be overwritten, and you need to pay attention. If the programmers of a project agree to use cache line alignment, it can also improve the efficiency of the C library.
If you determine that certain cache lines will not be used in the future, you can use instructions to mark them as invalid, and they will be replaced first next time, leaving room for others. But the entire line must be replaced. Another point is that you can use some 64-bit floating-point registers and instructions to read and write, which can be faster than 32-bit general-purpose registers.
Let’s talk about ddr access optimization. Usually software engineers think that memory is a device where all addresses have equal access time. Is this true? It depends. When we buy memory, there are three performance parameters, such as 10-10-10. This represents the three operation times required to access an address, row strobe, data delay and precharge. The first two are easy to understand, but the third one means that this page or unit will not be used next time I access it. It must be closed to maintain the capacitor voltage, otherwise the data will be lost when this page is used again. The DDR address consists of three parts, column, row, and page. According to this principle, if consecutive accesses are on the same page in the same row, each time only takes 10 units; on the same page in different rows, 20 units; on different pages in the same row, 30 units. So what conclusion do we draw? Adjacent data structures should be placed on the same page, and the occurrence of different pages in the same row must be avoided. How to calculate this? Each processor has a manual. Just look up the mapping of physical memory addresses to memory pins and deduce it. In addition, DDR also has a burst mode. DDR3 is an example. With 64-bit bandwidth, one command can be followed by 8 reads, which can fill a 64-byte cache line at once. In the extreme case (same page access), the average byte access time is only 10/64, which is three times worse than the worst case, 30/64 bytes. Of course, there are many tricks in memory, such as deliberately hashing addresses to prevent worst-case access, starting two memory controllers at the same time, and interleaving addresses to form pipeline access, etc., etc., which are all optimization methods. However, usually due to the existence of the scheduler, the addresses of the programs we run are relatively random and do not require such optimization. Optimization sometimes has negative effects. In addition, if all data is used only once, the bottleneck becomes memory access bandwidth, not cache. So the graphics card does not emphasize cache size. Of course, it also has a register file, which is similar to a cache, but not as big.
Every modern processor has a hardware memory management unit. To put it bluntly, it has two functions: providing address mapping when virtual addresses arrive and mapping real addresses to peripheral modules. It doesn't matter how complicated the definition of each field is, just care about what the given virtual address eventually becomes as a real address. I would like to say here that the memory management module of powerpc is really designed to be concise and clear. In comparison, x86 is too wordy and requires compatibility with so many modes. Of course, there is no other way. Processors in the communications field do not need much compatibility. Usually the memory management optimization we can use is to define a large hardware page table and include all addresses that need to be frequently used, so that there will be no page missing and the time of page missing exception calls and page table lookup will be saved. It can improve a lot of efficiency in certain situations.
The slowest memory access is described here: L1/2/3 cache miss -> hardware page table miss -> page fault exception code is not in cache -> read code -> software page table is not in cache -> read software page Table -> final read. At the same time, if the data accessed in each step is consistent across multiple cores, the front-side bus will spend more than ten cycles each time notifying the cache of each core to see if there is dirty data. In this way, thousands of clock cycles are required. If the slowest memory access occurs frequently, the previous optimization is very useful, saving dozens of times of time. You need to read the processor manual for the specific mapping method, so I won’t go into details.
There are many instruction optimizations, and each processor has a lot of them. Common ones include single instruction multiple data streams, specific operation instructions, branch instructions, etc. You need to read the manual of each processor, which is very detailed. I have data here, Fast Fourier Transform. If you use soft floating point on powerpc, the performance is 1, then use the built-in vector arithmetic coprocessor (the computing power is not strong, it is a low-cost replacement module for floating point devices) Finally, gcc automatically compiles and the performance is improved by 5 times. Then we manually wrote the assembly optimization function library and used vector instructions extensively, which improved the performance by another 14 times. The 70-fold improvement is enough to show the importance of pure instruction optimization.
GCC has three or four optimization levels. Generally, using O2 is a better balance. O3 may disrupt the original order of the program, making debugging very troublesome. You can look at the GCC help, which will explain each optimization, so I won’t go into details here. When compiling, you can try both, there may be a few percent difference.
Finally, there is the performance description tool. Under Linux, the most commonly used ones should be KProfile/OProfile. Its principle is to hit a point at a fixed time, see where the program has gone, and tell you the statistical results after enough time. From this, we can know which functions in the program are hot spots, what proportion of the execution time they occupy, and what the IPC of the specific code is. IPC means instructions per cycle. On a dual-launch powerpc, the theoretical maximum is 2. In fact, it would be very good if the overall value can reach 1.1. If it is too low, specific reasons need to be found. In this regard, relying on Profile is not enough. It cannot accurately count cache hits, instruction cycles, branch prediction hit rates, etc., and the accuracy is not high, which can sometimes be misleading. At this time, you need to use the performance statistics register that comes with the processor. The processor manual will describe usage in detail. With these data, we will continue to improve and compare the results to finally achieve the desired effect.
It is very important that we cannot rely on tools as the only means of judgment. Many times, it is necessary to optimize at one or several higher levels. For example, after working hard to optimize an algorithm to maximize the utilization of the processor and improve performance by 20%, it turns out that the complexity of the algorithm itself is too high. If the algorithm is improved, the improvement may be several times higher. Also, before optimizing, you must first have a clear understanding of the data flow, and then use tools to confirm this understanding. Just like designing a front-end digital module, you must first have a rough model in your mind, and then use a description language to implement it, rather than writing the code and comprehensively looking at the results.
Under this section, methods to improve transmission rate include:
Cache alignment to reduce the number of accesses.
Re-schedule memory access order and merge similar addresses to improve efficiency.
Increase DDR frequency to reduce latency
.
Use multiple controllers
to increase bandwidth. Enable DDR3 read and write command merging
. Enable burst mode to complete cache line access in one go.
Instruction and data prefetching improves idle utilization.
When the memory has ECC, use the same instruction write as the memory bit width (such as 64 bits). Otherwise, an additional read operation is required.
The controller alternately accesses, such as accessing the first 64 bits. Bit data is placed on the first memory controller and the second on the second controller so it can be staggered.
Physical address hashing prevents DDR from repeatedly opening and closing too many banks.
There is also an ultimate trick, calculate the physical address, and place the relevant data structure in the same physical page of DDR to reduce the probability of steps 1 and 3 in the three key steps of DDR transmission (row selection, command, precharge).