ARM攒机指南-开篇
经常有人说,现在做手机芯片就像搭积木,买点IP,连一下,后端外包。等芯片回来,上电,起操作系统,大功告成。这么简单,要不我们也来动手攒一颗吧。不过在攒机之前,我们还是先要把基础概念捋顺了。
评价一颗芯片,着眼点主要是功能,性能,功耗和价格。功能,是看芯片内部有什么运算模块,比如处理器,浮点器,编解码器,数字信号处理器,图形加速器,网络加速器等,还要看提供了什么接口,比如闪存,内存,PCIe,USB,SATA,以太网等。
性能,对CPU来说就是基准测试程序能跑多少分,比如Dhrystone,Coremark,SPEC2000/2006等。针对不同的应用,比如手机,还会看图形处理器的跑分,而对网络处理器,会看包转发率。当然,还需要跑一些特定的应用程序,来得到更准确的性能评估。
功耗,就是在跑某个程序的时候,芯片的功率是多少瓦。通常,这时候处理器会跑在最高频率,但这并不意味着所有的晶体管都在工作,由于power gating和clock gating的存在,有些没有被用到的逻辑和片上内存块并没在耗电。芯片公司给出的处理器功耗,通常都是在跑Dhrystone。这个程序有个特点,它只在一级缓存之上运行,不会访问二级缓存,更不会访问内存。这样得出的功耗,其实并不是包含了内存访问的真实功耗,也不是最大功耗。为得到处理器最大功耗,需要运行于一级缓存之上的向量和浮点指令,其结果通常是Dhrystone功耗的2-3倍。但是从实际经验看,普通的应用程序并不能让处理器消耗更高的能量,所以用Dhrysone测量也没什么问题。当然,要准确衡量整体的芯片功耗,还得考虑各种加速器,总线和接口,并不仅仅是处理器。
在芯片设计阶段,功能,性能,功耗和价格就转换成了PPA。PPA指的是性能,功耗和面积。其中,性能有两层含义。在前端设计上,它表示的是每赫兹能够跑多少标准测试程序分。设计处理器的和时候,会有多少级流水线的说法。通常来说,流水线级数越多,芯片能跑到的最高频率越高。可是并不是频率越高,性能就越高。这和处理器构架有很大关系。典型的反例就是Intel的奔腾4,30多级流水,最高频率高达3G赫兹,可是由于流水线太长,一旦指令预测错误,重新抓取的指令要重走这几十级流水线,代价是很大的。而它的指令又非常依赖于编译器来优化,当时编译器又没跟上,导致总体性能低下。而MIPS或者PowerPC的处理器频率都不高,但是每赫兹性能相对来说还不错,总体性能就会提高一些。所以性能要看总体跑分,而不是每赫兹跑分。
性能的另外一个含义就是指最高频率,这是从后端设计角度来说的。通常后端的人并不关心每赫兹能达到多少跑分,只看芯片能跑到多少频率。频率越高,在每赫兹跑分一定的情况下,总体性能就越高。请注意对于那些跑在一级缓存的程序,处理器每赫兹跑分不会随着频率的变化而变化。而如果考虑到多级缓存,总线和外围接口,那肯定就不是随处理器频率线性增加了。
哪些因素会影响频率?就算只从后端角度考虑,因素也很多,以下方面仅供参考。
首先,受工艺的影响。现在先进的半导体工厂就那么几家,Intel,台积电,三星,格芯,联电等。拿台积电来说,它之前提供16纳米的工艺,其中还分了很多小结点,比如FFLL++和FFC。每个小节点各有特点,有些能跑到更高频率,有些功耗低,有些成本低。在不同的工艺上,芯片能跑的最高频率不同,功耗和面积也不同。
其次,受后端库的影响。台积电会把工艺中晶体管的参数抽象出来,做成一个物理层开发包,提供给工具厂商,IP厂商和芯片厂商。而这些厂商的后端工程师,就会拿着这个物理层开发包,做自己的物理库。物理库一般包含逻辑和内存两大块。根据晶体管参数的不同,会有不同特性,适合于不同的用途。而怎么把这些不同特性的的库,合理的用到各个前端设计模块,就是一门大学问。一般来说,源极和漏极通道越短,电子漂移距离越短,能跑的频率就越高。可是,频率越高,动态功耗就越大,并且可能是按指数级上升。除此之外,还会有Track这种说法,指的是的标准单元的宽度。宽度越大,电流越大,越容易做到高频,面积也越大。还有一个可调的参数就是阈值电压,决定了栅极的电压门限,门限越低,频率能冲的越高,静态功耗也越大,按对数级上升。
接下来,受布局和布线的影响。芯片里面和主板一样,也是需要多层布线的,每一层都有个利用率。总体面积越小,利用率越高,布线就越困难。而层数越多,利用率越低,成本就越高。在给出一些初始和限制条件后,EDA软件会自己去不停的计算,最后给出一个可行的频率和面积。
再次,受前后端协同设计的影响。处理器的关键路径直接决定了最高频率。ARM的大核,A73之后,由于采用了虚地址索引VIPT,免去了查MMU,关键路径已经集中到一级缓存的访问时间延迟上了。
从功耗角度,同样是前后端协同设计,某个访问片上内存的操作,如果知道处理器会花多少时间,用哪些资源,就可以让内存的空闲块关闭,从而达到省电的目的。这种技巧可能有上千处,只有自己做处理器才会很清楚。
再往上,就是动态电压频率缩放DVFS。这里需要引入功耗的组成概念。芯片功耗分成动态和静态两部分,静态就是晶体管漏电造成的,大小和芯片工艺,晶体管数,电压相关,而动态是开关切换造成的,所以和晶体管数,频率,电压相关。控制动态功耗的方法是clock gating,频率变小,自然动态功耗就小。控制静态功耗的方法是power gating,关掉电源,那么静态和动态功耗都没了。还可以降低电压,那么动态功耗和静态功耗自然都小。可是电压不能无限降低,否则电子没法漂移,晶体管就不工作了。并且,晶体管跑在不同的频率,所需要的电压是不一样的,拿16纳米来说,往下可以从0.9V变成0.72V,往上可以变成1V或者更高。别小看了这一点点的电压变化,动态功耗的变化,是和电压成2次方关系,和频率成线性关系的。而频率的上升,同样是依赖于电压提升的。所以,1.05V和0.72V,电压差了45%,动态功耗可以差3倍。
再往上,就是软件电源管理了。芯片设计者把每个大模块的clock gating和power gating进行组合,形成不同的休眠状态,软件可以根据温度和运行的任务,动态的告诉处理器每个模块进入不同的休眠状态,从而在任务不忙的时候降低功耗。
从上面我们可以看到,功耗和性能其实是相辅相成的。芯片设计者可以用不同的工艺和物理库,在给定功耗下,设计出最高可运行频率,然后用软件动态控制芯片运行频率和电压,优化功耗。
频率和面积其实也是互相影响的。给定一个目标频率,选用了不同的物理库,不同的track,不同的利用率,形成的芯片面积就会不一样。通常来说,越是需要跑高频的芯片,所需的面积越大。频率差一倍,面积可能有百分之几十的差别。别小看这百分之几十,对晶体管来说,面积就是成本,晶圆的总面积一定,价钱一定,那单颗芯片的面积越小,成本越低,并且此时良率也越高。
芯片成本除了流片,晶圆和封测费,还来自于授权费,工具费,运营开销等,通常手机处理器这样复杂的芯片,没有几千万美元是不可能做出来的。就算做出来,没有卖掉几百万片,也肯定是亏本的。
这里再提下ARM的大小核设计。其最初的目的是想设计两组核,小核每赫兹性能低,面积小,跑在低频;大核每赫兹性能高,面积大,跑在高频。运行简单任务,大核关闭,小核在低频,动态和静态功耗都低,而大核用高频运行复杂任务。小核在低功耗场景下,通常只需要大核一半的面积和五分之一的功耗。这和不区分大小核,单纯调节电压频率比,有显著优势。
那为什么不让小核跑在高频运行复杂任务呢?理论上,由于每赫兹性能低,对于相同的任务,小核必须跑在比大核更高的频率才能完成,这就意味着更高的电压。此时,动态功耗占上风,并且和电压成三次方关系,最终的功耗会高出大核不少。此外,我们前面已经解释过,小核要跑在高频,面积会增大不少,可能比大核还要大。所以,这里存在一个平衡点。拿A53/A57在28纳米上举例,当它们跑在1.2Ghz的时候,功耗可能差两倍,性能却只差50%。而继续升频,功耗3次方上升,性能线性上升,最终可能在2Ghz达到平衡点。此时,A53的能效比反而不如A57。当然,这个平衡点在不同工艺上是不断变化的。再反过来考虑,在2Ghz之前,其实可以用高频A53做大核,能效比并不低于A57。事实上,很多手机芯片已经这么做了。
还有一个问题,既然小核能效比更高,那为什么不用多个小核来代替大核呢?这是因为手机上的很多应用,如果没有特别优化,都是单线程的,多线程编程向来容易出问题。此时,多个小核并不能代替一个大核,所以大核必须存在。而当应用适合分成多线程,也没有过多同步的开销时,毫无疑问,小核更具能效比。
从上面我们看到,设计芯片很大程度上就是在平衡。影响因素,或者说坑,来自于方方面面,IP提供商,工厂,市场定义,工程团队。水很深,坑很大,没有完美的芯片,只有完美的平衡。在这点上,苹果是一个很典型的例子。苹果A10的CPU频率不很高,但是Geekbench单核跑分却比 A73高了整整75%,接近Intel桌面处理器的性能。为什么?因为苹果用了大量的面积换取性能和功耗。首先,它使用了六发射,而A73只有双发射,流水线宽了整整三倍。当然,三倍的发射宽度并不表示性能就是三倍,由于数据相关性的存在,发射宽度的效益是递减的。再一点,苹果使用了整整6MB的缓存,而这个数字在别的手机芯片上通常是2MB。对一些标准跑分,比如SpecInt2000/2006,128KB到256KB二级缓存带来的性能提升仅仅是7%左右,而256KB到1MB带来的提升更小,缓存面积却是4倍。第三,除了一二三级缓存之外,苹果大量增加处理器在各个环节的缓冲,比如指令预测器等。当然,面积的提升同样带来了静态功耗的增加,不过相对于提升频率,造成动态功耗增加来说,还是小的。再次,苹果引入的复杂的电源,电压和时钟控制,虽然增加了面积,但由于系统软件都是自己的,可以从软件层面就进行很精细的优化,将整体功耗控制的非常好。举个例子,Wiki上面可以得知,A10上的大核Hurricane面积在TSMC的16nm上是4.18 平方毫米,而ARM的Enyo去掉二级缓存差不多是2.4平方毫米,在2.4Ghz时,SPECINT2000跑分接近,面积差了70%。但是,也只有苹果能这么做,一般芯片公司绝对不会走苹果这样用大量面积换性能和功耗的路线,那样的话毛利就太低了。
ARM攒机指南-基础篇
在开篇里,我们对芯片PPA有了初步的认识。下面,让我们从访存这个简单的问题开始展开介绍芯片基础概念。
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没满,还是一个时钟周期。当然,如果流水线上某个节拍大于一个时钟周期,那平均的延时就会取决于这个最慢的时间。在读取二级,三级缓存和内存的时候,我们可以把等待返回看作一个节拍,那么就能很自然的理解此时的延迟了。由此,我们可以得到每一级缓存的延迟和访存延迟。
上图画了ARM某处理器读写指令经过的单元,简单流程如下:
当写指令从存取单元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字节。
那这个结论是不是正确?写个小程序测试下就知道。我们可以关掉二级缓存,保留一级缓存,然后用以下指令去读取一个较大的内存区域。所有的地址都是缓存行对齐。不对齐,甚至越过缓存行边界,会把一个操作变成两个,肯定会慢。伪代码如下:
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字节,2xLinefill buffer是32字节。测试得到的命中率是12%多点。也完全符合估算。
但是为什么R5要设计两个32字节长度的Linefill
buffer?既然它的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可以更大,那就是缓存的硬件预取。当程序访问连续的或者有规律的地址时,缓存会自动检测出这种规律,并且预先去把数据取来。这种方法同样不占用处理器时间,但是也会占用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模式其实存在一个问题,那就是它把数据写到了下一级缓存或者内存,万一这个数据马上就会被使用呢?那岂不是还得去抓取?针对这个问题,在ARMv8指令集中,又引入了新的一条缓存操作指令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字节,可以跑在2Ghz左右,和处理器同频。处理器频率再快,那么访问缓存就需要2-3个处理器周期了。但由于访问一级缓存的时间一般不会超过3个始终周期,每增加一个周期,性能就会有明显的下降。而二级缓存更慢,256K字节的,能有800Mhz就很好了。这是由于缓存越大,需要查找的目录index越大,扇出fanout和电容越大,自然就越慢。但由于访问二级缓存本身的延迟就有10个时钟周期左右,多一个周期影响没有那么明显。还有,通常处理器宣传时候所说的访问缓存延迟,存在一个前提,就是使用虚拟地址索引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处理器周期,已经慢于至强了。其原因是访问硬件加速器本身需要设置4-8次的寄存器,而访问一次寄存器的延迟是几十纳秒,反而成为了瓶颈。
如果上面一段看完你没什么感觉,那我可以换个说法:对于没有完整支持stashing的ARM SoC,哪怕处理器跑在10Ghz,网络加速器性能强的翻天,基于DPDK的简单包转发(快于Linux内核网络协议栈转发几十倍)还是只能到至强的30%,而包转发是网络处理器的最重要的指标之一,也是服务器跑网络转发软件的指标之一,更可以用在存储领域,加速SPDK之类的存储应用。
还有,在ARM新的面向网络和服务器的核心上,会出现一核两线程的设计。处理包的任务天然适合多线程,而一核两线程可以更有效的利用硬件资源,再加上stashing,如虎添翼。
弄清了访存的路径,可能就会想到一个问题:处理器发出去的读写请求到底是个什么东西?要想搞清楚它,就需要引入总线。下文我拿ARM的AXI/ACE总线协议以及由它衍生的总线结构来展开讨论。这两个协议广泛用于主流的手机芯片上,是第四代AMBA(Advanced Microcontroller Bus Architecture)标准。
简单的总线就是一些地址线和数据线,再加一个仲裁器,就可以把处理器发过来的读写请求送到内存或者外设,再返回数据。在这个过程中,我们需要一个主设备,一个从设备,所有的传输都是主设备发起,从设备回应。让我们把处理器和它包含的缓存看作一个主设备,把内存控制器看作从设备。处理器发起访问请求,如果是读,那么总线把这个请求(包括地址)送到内存控制器,然后等待回应。过了一段时间,内存控制器把内存颗粒里面读出的数据交给总线,总线又把数据交给处理器。如果数据无误(ECC或者奇偶校验不出错),那么这个读操作就完成了。如果是写,处理器把写请求(包括地址)和数据交给总线,总线传递给内存控制器,内存控制器写完后,给出一个确认。这个确认经由总线又回到了处理器,写操作完成。
以上过程有几个重点。第一,处理器中的单个读指令,被分为了请求(地址),完成(数据)阶段。写指令也被分为了请求(地址,数据),完成(写入确认)阶段。第二,作为从设备,内存控制器永远都无法主动发起读写操作。如果一定要和处理器通讯,比如发生了读写错误,那就得使用中断,然后让处理器来发起读写内存控制器状态的请求。第三,未完成的读写指令就变成了OT,总线可以支持多个OT。然而,总线支持多OT并不表示处理器能发送这么多请求出来,尤其是读。所以瓶颈可能还是在处理器。
我遇到过几次这样的情况,在跑某个驱动的时候,突然系统挂死。但是别的设备中断还能响应,或者报个异常后系统又继续跑了。如果我们把上文的内存控制器替换成设备控制器,那就不难理解这个现象了。假设处理器对设备发起读请求,而设备没有回应,那处理器就会停在那等待。我看到的处理器,包括PowerPC, ARM,都没有针对这类情况的超时机制。如果没有中断,那处理器无法自己切换到别的线程(Linux等操作系统的独占模式),就会一直等待下去,系统看上去就挂住了。有些设备控制器可以自动探测这类超时,并通过中断调用相应的异常或者中断处理。在中断处理程序中,可以报个错,修改返回地址,跳过刚才的指令往下走,系统就恢复了。也有些处理器在触发某类异常后能自动跳到下一行指令,避免挂死。但是如果没有异常或者中断发生,那就永远挂在那。
继续回到总线。在AXI/ACE总线协议中,读和写是分开的通道,因为他们之间并没有必然联系。更细一些,总线上规定了五个组,分别是读操作地址(主到从),读操作数据(从到主),写操作地址(主到从),写操作数据(主到从),写操作确认(从到主)。读和写两大类操作之间,并没有规定先后次序。而在每一类操作之内的组之间,是有先后次序的,比如地址是最先发出的,数据随后,可以有很多拍,形成突发操作。而确认是在写操作中,从设备收到数据之后给出的。对内存控制器,必须在数据最终写入到颗粒之后再给确认,而不是收到数据放到内部缓存中的时候。当然,这一点可以有例外,那就是提前应答early
response。中间设备为了提高效,维护自己的一块缓冲,在收到数据后,直接向传递数据的主设备确认写入,使得上层设备释放资源。但是这样一来,由于数据并没有真正写入最终从设备,发出提前应答的中间设备必须自己维护好数据的一致性和完整性,稍不小心就会造成死锁。ARM的现有总线都不支持这个操作,都是不会告知主设备early response的,所有的内部缓冲,其实是一个FIFO,不对访问次序和应答做任何改动。
对于同一个通道,如果收到连续的指令,他们之间的次序是怎么样的呢?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/CMN系列是基于环状和网状总线的(Ring/Mesh),NoC系列是包转发总线(Router)。他们各有特点,适合不同场景。交叉矩阵连接的主从设备数量受到限制,但是效率最高,读写请求可以在1到2个周期内就直达从设备。如下图所示,这就是一个5x4的交叉矩阵:
根据我看到的数据,在28纳米制程上,5x4的配置下,这个总线的频率可以跑到300Mhz。如果进一步增加主从对数量,那么由于扇出增加,电容和走线增加,就必须通过插入更多的寄存器来增加频率。但这样一来,从主到从的延迟就会相应增加。哪怕就是保持5x3的配置,要想进一步提高到500Mhz,要么使用更好的工艺,16纳米我看到的是800Mhz;要么插入2-3级寄存器,这样,读写延时就会达到4-5个总线时钟周期,请求加完成来回总共需要10个。如果总线和处理器的倍频比率为1:2,那么仅仅是在总线上花费的时间,就需要至少20个处理器时钟周期。倍率为4,时间更长,40个时钟周期。要知道处理器访问二级缓存的延迟通常也不过10多个处理器周期。当然,可以通过增加OT数量减少平均延迟,可是由于处理器的OT数是有限制的,对于顺序处理器,可能也就是1-2个。所以,要达到更高的频率,支持更多的主从设备,就需要引入环状总线CCN系列,如下图:
CCN总线上的每一个节点,除了可以和相邻的两个节点通讯之外,还可以附加两个节点组件,比如处理器组,三级缓存,内存控制器等。在节点内部,还是交叉的,而在节点之间,是环状的。这样使得总线频率在某种程度上摆脱了连接设备数量的限制(当然,还是受布线等因素的影响),在16纳米下,可以达到1.2GHz以上。当然,代价就是节点间通讯更大的平均延迟。为了减少平均延迟,可以把经常互相访问的节点放在靠近的位置。
在有些系统里,要求连接更多的设备,并且,频率要求更高。此时环状总线也不够用了,这时需要网状总线CMN。ARM的网状总线,符合AMBA5.0的CHI接口,支持原子操作(直接在缓存运算,不用读取到处理器),stashing和直接访问(跳过中间的缓存,缩短路径)等特性,适用于服务器或者网络处理器。
但是有时候,系统需要连接的设备数据宽度,协议,电源,电压,频率,都不一样,这时就需要NoC出马了,如下图:
这个图中,刚才提到的交叉矩阵,可以作为整个网络的某部分。而连接整个系统的,是位于NoC内的节点。每个节点都是一个小型路由,它们之间传输的,是异步的包。这样,就不必维持路由和路由之间很大数量的连线,从而提高频率,也能支持更多的设备。当然,坏处就是更长的延迟。根据我看到的数据,在16纳米上,频率可以跑到1.5Ghz。并且它所连接每个子模块之间,频率和拓扑结构可以是不同的。可以把需要紧密联系的设备,比如CPU簇,GPU放在一个子网下减少通讯延迟。
在实际的ARM生态系统中,以上三种拓扑结构的使用情况是怎么样的呢?一般手机芯片上使用交叉矩阵,网络处理器和服务器上使用环状和网状拓扑,而NoC也被大量应用于手机芯片。最后一个的原因倒不是手机上需要连接的设备数太多,而是因为ARM的AXI总线NIC400对于交叉访问(interleaving)支持的非常有限。在手机里面,GPU和显示控制器对内存带宽要求是很高的。一个1080p的屏幕,每秒要刷新60次,2百万个像素,每个像素32比特颜色,再加上8层图层,就需要4GB/s的数据,双向就是8GB/s。而一个1.6GHz传输率的LPDDR4控制器,64位数据,也只能提供12.8GB/s的的理论带宽。理论带宽和实际带宽由于各种因素的影响,会有很大差别,复杂场景下能做到70%的利用率就不错了,那也就是9GB/s。那处理器怎么办?其他各类控制器怎么办?只能增加内存控制器的数量。但是,不能简单的增加数量。成本和功耗是一个原因,并且如果仅仅把不同的物理地址请求发送到不同的内存控制器上,很可能在某段时间内,所有的物理地址全都是对应于其中某一个,还是不能满足带宽要求。解决方法就是,对于任何地址,尽量平均的送到不同的内存控制器。并且这件事最好不是处理器来干,因为只有总线清楚有多少个内存控制器。最好处理器只管发请求,总线把所有请求平均分布。
有时候,传输块大于256字节,可以采用一个方法,把很长的传输拆开(Splitting),分送到不同的内存控制器。不幸的是,AXI总线天然就不支持一对多的访问。原因很简单,会产生死锁。想象一下,有两个主设备,两个从设备,通过交叉矩阵连接。M1发送两个读请求,标志符都是1,先后送到到S1和S2,并等待完成。然后M2也做同样的事情,标志符都是2,先后送到S2和S1。此时,假设S2发现它如果把返回的数据次序交换一下,会更有效率,于是它就这么做了。但是M1却不能接收S2的返回数据,因为根据同标志符必须顺序完成的原则,它必须先等S1的返回数据。而S1此时也没法送数据给M2,因为M2也在等待S2返回的数据,死锁就出现了。解决方法是,AXI的Master不要发出相同标志的操作。如果标志相同时,则必须等待上一次操作完成。或者,拆分和设置新标识符操作都由总线来维护,而主设备不关心,只管往外发。
在实际情况下,拆分主要用于显示,视频和DMA。ARM的CPU和GPU永远不会发出大于64字节的传输,不需要拆分。
现在的中低端手机很多都是8核,而根据ARM的设计,每个处理器组中最多有四个核。这就需要放两个处理器组在系统中,而他们之间的通讯,包括大小核的实现,就需要用到总线一致性。每个处理器组内部也需要一致性,原理和外部相同,我就不单独解释了。使用软件实可以现一致性,但是那样需要手动的把缓存内容刷到下一级缓存或者内存,对于一个64字节缓存行的64KB缓存来说,需要1000次刷新,每次就算是100纳秒,且OT=4的话,也需要25微秒。对处理器来说这是一个非常长的时间。ARM使用了一个协处理器来做这个事情,这是一个解决方案。为了用硬件解决,ARM引入了几个支持硬件一致性的总线,下图是第一代方案CCI400:
CCI400是怎么做到硬件一致性的呢?简单来说,就是处理器组C1,发一个包含地址信息的特殊读写的命令到总线,然后总线把这个命令转给另一个处理器组C2。C2收到请求后,根据地址逐步查找二级和一级缓存,如果发现自己也有,那么就返回数据或者做相应的缓存一致性操作,这个过程称作snooping(监听)。具体的操作我不展开,ARM使用MOESI一致性协议,里面都有定义。在这个过程中,被请求的C2中的处理器核心并不参与这个过程,所有的工作由缓存和总线接口单元BIU等部件来做。为了符合从设备不主动发起请求的定义,需要两组主从设备,每个处理器组占一个主和一个从。这样就可以使得两组处理器互相保持一致性。而有些设备如DMA控制器,它本身不包含缓存,也不需要被别人监听,所以它只包含从设备,如上图桔黄色的部分。在ARM的定义中,具有双向功能的接口被称作ACE,只能监听别人的称作ACE-Lite。它们除了具有AXI的读写通道外,还多了个监听通道,如下图:
多出来的监听通道,同样也有地址(从到主),回应(主到从)和数据(主到从)。每组信号内都包含和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中实现,示意图如下:
在经过实际性能测试后,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总线中是不支持壁垒的,必须在芯片设计阶段,通过配置选项告诉处理器自己处理壁垒指令,不要送到总线。但这并不影响程序中的壁垒指令,处理器会在总线之前把它过滤掉。
具体到CCI总线上,壁垒机制是怎么实现的呢?首先,壁垒和读写一样,也是使用读写通道的,只不过它地址总是0,且没有数据。标志符也是有的,此外还有额外的2根线BAR0/1,表明本次传输是不是壁垒,是哪种壁垒。他是怎么传输的呢?先看弱壁垒,如下图:
Master0写了一个数据data,然后又发了弱壁垒请求。CCI和主设备接口的地方,一旦收到壁垒请求,立刻做两件事,第一,给Master0发送壁垒响应;第二,把壁垒请求发到和从设备Slave0/1的接口。Slave1接口很快给了壁垒响应,因为它那里没有任何未完成传输。而Slave0接口不能给壁垒响应,因为data还没发到从设备,在这条路径上的壁垒请求必须等待,并且不能和data的写请求交换次序。这并不能阻挠Master0发出第二个数据,因为它已经收到它的所有下级(Master0接口)的壁垒回应,所以它又写出了flag。如下图:
此时,flag在Master0接口中等待它的所有下一级接口的壁垒响应。而data达到了Slave0后,壁垒响应走到了Master0接口,flag继续往下走。此时,我们不必担心data没有到slave0,因为那之前,来自Slave0接口的壁垒响应不会被送到Master0接口。这样,就做到了弱壁垒的次序保证,并且在壁垒指令完成前,flag的请求就可以被送出来。
对于强壁垒指令来说,仅仅有一个区别,就是Master0接口在收到所有下一级接口的壁垒响应前,它不会发送自身的壁垒响应给Master0。这就造成flag发不出来,直到壁垒指令完成。如下图:
这样,就保证了强壁垒完成后,下一条读写指令才能发出请求。此时,强壁垒前的读写指令肯定是完成了的。
另外需要特别注意的是,ARM的弱壁垒只是针对显式数据访问的次序。什么叫显式数据访问?读写指令,缓存,TLB操作都算。相对的,什么是隐式数据访问?在处理器那一节,我们提到,处理器会有推测执行,预先执行读写指令;缓存也有硬件预取机制,根据之前数据访问的规律,自动抓取可能用到的缓存行。这些都不包含在当前指令中,弱壁垒对他们无能为力。因此,切记,弱壁垒只能保证你给出的指令次序,并不能保证在它们之间没有别的模块去访问内存,哪怕这个模块来自于同一个核。
简单来说,如果只需要保证读写次序,用弱壁垒;如果需要某个读写指令完成才能做别的事情,用强壁垒。以上都是针对普通内存类型。当我们把类型设成设备时,自动保证强壁垒。
我们提到,壁垒只是针对单核。在多核多线程时,哪怕使用了壁垒指令,也没法保证读写的原子性。解决办法有两个,一个是软件锁,一个是原子操作。AXI/ACE协议不支持原子操作。所以手机通常需要用到软件锁。
软件锁中有个自旋锁,能用一个ARM硬件机制exclusive access来实现。当使用特殊指令对一个地址写入值,相应缓存行上会做一个特殊标记,表示还没有别的核去写这行缓存。然后下条指令读这个行,如果标记没变,说明写和读之间没有人打扰,那么就拿到锁了。如果变了,那么回到写的过程重新获取锁。由于缓存一致性,这个锁变量可以被多个核与线程使用。当然,过程中还是需要壁垒指令来保证次序。
在支持ARMv8.2和AMBA 5.0 CHI接口的系统中,原子操作被重新引入。在硬件层面,其实原子操作非常容易理解,如果某个数据存在于自己的缓存,那就直接修改;如果存在于别人的缓存,那对所有其他缓存执行Eviction操作,踢出后,放到自己的缓存继续操作。这个过程其实和exclusive access非常类似。
对于普通内存,还会产生一个问题,就是读写操作可能会经过缓存,你不知道数据是否最终写到了内存中。通常我们使用clean操作来刷缓存。但是刷缓存本身是个模糊的概念,缓存存在多级,有些在处理器内,有些在总线之后,到底刷到哪里算是终结呢?还有,为了保证一致性,刷的时候是不是需要通知别的处理器和缓存?为了把这些问题规范化,ARM引入了Point of Unification/Coherency,Inner/Outer Cacheable和System/Inner/Outer/Non Shareable的概念。
PoU是指,对于某一个核Master,附属于它的指令,数据缓存和TLB,如果在某一点上,它们能看到一致的内容,那么这个点就是PoU。如上图右侧,MasterB包含了指令,数据缓存和TLB,还有二级缓存。指令,数据缓存和TLB的数据交换都建立在二级缓存,此时二级缓存就成了PoU。而对于上图左侧的MasterA,由于没有二级缓存,指令,数据缓存和TLB的数据交换都建立在内存上,所以内存成了PoU。还有一种情况,就是指令缓存可以去监听数据缓存,此时,不需要二级缓存也能保持数据一致,那一级数据缓存就变成了PoU。
PoC是指,对于系统中所有Master(注意是所有的,而不是某个核),如果存在某个点,它们的指令,数据缓存和TLB能看到同一个源,那么这个点就是PoC。如上图右侧,二级缓存此时不能作为PoC,因为MasterB在它的范围之外,直接访问内存。所以此时内存是PoC。在左图,由于只有一个Master,所以内存是PoC。
再进一步,如果我们把右图的内存换成三级缓存,把内存接在三级缓存后面,那PoC就变成了三级缓存。
有了这两个定义,我们就可以指定TLB和缓存操作指令到底发到哪个范围。比如在下图的系统上,有两组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还是outer shareable,都会经由CCI广播到别的ACE口上。
说了这么多概念,你可能会想这有什么用处?回到上文的Clean指令,PoU使得四个A7的指令缓存中对应的行都被清掉。由于是指令缓存操作,Inner Shareable属性使得这个操作被扩散到总线。而CCI400总线会把这个操作广播到所有可能接受的口上。ACE口首当其冲,所以四个A15也会清它们对应的指令缓存行。对于Mali和DMA控制器,他们是ACE-Lite,本不必清。但是请注意它们还连了DVM接口,专门负责收发缓存维护指令,所以它们的对应指令缓存行也会被清。不过事实上,它们没有对应的指令缓存,所以只是接受请求,并没有任何动作。
要这么复杂的定义有什么用?用处是,精确定义TLB/缓存维护和读写指令的范围。如果我们改变一下,总线不支持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最大的好处是可以和性能计数器的统计结果精确匹配,看看是不是达到预期了。
现在手机和平板上最常见的用法,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条件,从而在一定程度上减少延迟。我们也可以通过计算和调整软件物理地址来避免上述情况的发生。
在实际的访问中,通常无法保证访问只在一个页中。DDR内存支持同时打开多个页,比如4个。而通过交替访问,我们可以同时利用这4个页,不必等到上一次完成就开始下一个页的访问。这样就可以减少平均延迟。如下图:
我们可以通过突发访问,让上图中的绿色数据块更长,那么相应的利用率就越高。此时甚至不需要用到四个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%左右。此外,由于多个主设备的存在,每一个主都产生不同的连续地址,使得效果进一步降低。因此,只有采用交织访问才能真正的实现均匀访问多个内存控制器。当然,此时的突发长度和粒度要匹配,不然粒度太大也没法均匀,就算均匀了也未必是最优的。对于某个内存控制来说,最好的期望是总收到同一个物理页内的请求。
还有一点需要提及。如果使用了带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%的时间都是要省的。
在访问指令或者数据的时候,有一个非常重要的事项,就是对齐。四字节对齐还不够,最好是缓存行对齐,一般是在做内存拷贝,DMA或者数据结构赋值的时候用到。处理器在读取数据结构时,是以行为单位的,长度可以是32字节或更大。如果数据结构能够调整为缓存行对齐,那么就可以用最少的次数读取。在DMA的时候一般都以缓存行为单位。如果不对齐,就会多出一些传输,甚至出错。还有,在SoC系统上,对有些设备模块进行DMA时,如果不是缓存行对齐,那么可能每32字节都会被拆成2段分别做DMA,这个效率就要差了1倍了。
如果使用了带ecc的内存,那么更需要ddr带宽对齐了。因为使能ecc后,所有内存访问都是带宽对齐的,不然ecc没法算。如果你写入小于带宽的数据,内存控制器需要知道原来的数据是多少,于是就去读,然后改动其中一部分,再计算新的ecc值,再写入。这样就多了一个读的过程,慢不少。
还有一种需要对齐情况是数据结构赋值。假设有个32字节的数据结构,里面全是4字节元素。正常初始化清零需要32/4=8次赋值。而有一些指令,可以直接把缓存行置全0或1。这样时间就变成1/8了。更重要的是,写缓存未命中实际上是需要先从内存读取数据到缓存,然后再写入。这就是说写的未命中和读未命中需要一样的时间。而用了这个指令,可以让存指令不再去读内存,直接把全0/1写入缓存。这在逻辑上是没问题的,因为要写入的数据(全0/1)已经明确,不需要去读内存。以后如果这行被替换出去,那么数据就写回到内存。当然,这个指令的限制也很大,必须全缓存行替换,没法单个字节修改。这个过程其实就是优化后的memset()函数。如果调整下你的大数据结构,把同一时期需要清掉的元素都放一起,再用优化的memset(),效率会高很多。同理,在memcpy()函数里面,由于存在读取源地址和写入目的地址,按上文所述,可能有两个未命中,需要访存两次。现在我们可以先写入一个缓存行(没有写未命中),然后再读源地址,写入目的地址,就变成了总共1个访存操作。至于写回数据那是处理器以后自己去做的事情,不用管。
标准的libc库里面的内存操作函数都可以用类似方法优化,而不仅仅是四字节对齐。不过需要注意的是,如果给出的源和目的地址不是缓存行对齐的,那么开头和结尾的数据需要额外处理,不然整个行被替换了了,会影响到别的数据。此外,可以把预取也结合起来,把要用的头尾东西先拿出来,再作一堆判断逻辑,这样又可以提高效率。不过如果先处理尾巴,那么当内存重叠时,会发生源地址内容被改写,也需要注意。如果一个项目的程序员约定下,都用缓存行对齐,那么还能提高C库的效率。
如果确定某些缓存行将来不会被用,可以用指令标记为无效,下次它们就会被优先替换,给别人留地。不过必须是整行替换。还有一点,可以利用一些64位浮点寄存器和指令来读写,这样可以比32为通用寄存器快些。
再说说ddr访问优化。通常软件工程师认为内存是一个所有地址访问时间相等的设备,是这样的么?这要看情况。我们买内存的时候,有3个性能参数,比如10-10-10。这个表示访问一个地址所需要的三个操作时间,行选通,数据延迟还有预充电。前两个好理解,第三个的意思是,我这个页或者单元下一次访问不用了,必须关闭,保持电容电压,否则再次使用这页数据就丢失了。ddr地址有三个部分组成,列,行,页。根据这个原理,如果连续的访问都是在同行同页,每一个只需要10单位时间;不同行同页,20单位;同行不同页,30单位。所以我们得到什么结论?相邻数据结构要放在一个页,并且绝对避免出现同行不同页。这个怎么算?每个处理器都有手册,去查查物理内存地址到内存管脚的映射,推导一下就行。此外,ddr还有突发模式,ddr3为例,64位带宽的话,可以一个命令跟着8次读,可以一下填满一行64字节的缓存行。而极端情况(同页访问)平均字节访问时间只有10/64,跟最差情况,30/64字节差了3倍。当然,内存里面的技巧还很多,比如故意哈希化地址来防止最差情况访问,两个内存控制器同时开工,并且地址交织来形成流水访问,等等,都是优化的方法。不过通常我们跑的程序由于调度程序的存在,地址比较随机不需要这么优化,优化有时候反而有负面效应。另外提一句,如果所有数据只用一次,那么瓶颈就变成了访存带宽,而不是缓存。所以显卡不强调缓存大小。当然他也有寄存器文件,类似缓存,只不过没那么大。
每个现代处理器都有硬件内存管理单元,说穿了就两个作用,提供虚地址到时地址映射和实地址到外围模块的映射。不用管它每个字段的定义有多么复杂,只要关心给出的虚地址最终变成什么实地址就行。在此我想说,powerpc的内存管理模块设计的真的是很简洁明了,相比之下x86的实在是太罗嗦了,那么多模式需要兼容。当然那也是没办法,通讯领域的处理器就不需要太多兼容性。通常我们能用到的内存管理优化是定义一个大的硬件页表,把所有需要频繁使用的地址都包含进去,这样就不会有页缺失,省了页缺失异常调用和查页表的时间。在特定场合可以提高不少效率。
这里描述下最慢的内存访问:L1/2/3缓存未命中->硬件页表未命中->缺页异常代码不在缓存->读取代码->软件页表不在缓存->读取软件页表->最终读取。同时,如果每一步里面访问的数据是多核一致的,每次前端总线还要花十几个周期通知每个核的缓存,看看是不是有脏数据。这样一圈下来,几千个时钟周期是需要的。如果频繁出现最慢的内存访问,前面的优化是非常有用的,省了几十倍的时间。具体的映射方法需要看处理器手册,就不多说了。
指令优化,这个就多了,每个处理器都有一大堆。常见的有单指令多数据流,特定的运算指令化,分支指令间化,等等,需要看每家处理器的手册,很详细。我这有个数据,快速傅立叶变化,在powerpc上如果使用软浮点,性能是1,那么用了自带的矢量运算协处理器(运算能力不强,是浮点器件的低成本替换模块)后,gcc自动编译,性能提高5倍。然后再手工写汇编优化函数库,大量使用矢量指令,又提高了14倍。70倍的提升足以显示纯指令优化的重要性。
GCC的优化等级有三四个,一般使用O2是一个较好的平衡。O3的话可能会打乱程序原有的顺序,调试的时候很麻烦。可以看下GCC的帮助,里面会对每一项优化作出解释,这里就不多说了。编译的时候,可以都试试看,可能会有百分之几的差别。
最后是性能描述工具。Linux下,用的最多的应该是KProfile/OProfile。它的原理是在固定时间打个点,看下程序跑到哪了,足够长时间后告诉你统计结果。由此可以知道程序里那些函数是热点,占用了多少比例的执行时间,还能知道具体代码的IPC是多少。IPC的意思是每周期多少条指令。在双发射的powerpc上,理论上最多是2,实际上整体能达到1.1就很好了。太低的话需要找具体原因。而这点,靠Profile就不行了,它没法精确统计缓存命中,指令周期数,分支预测命中率等等,并且精度不高,有时会产生误导。这时候就需要使用处理器自带的性能统计寄存器了。处理器手册会详细描述用法。有了这些数据,再不断改进,比较结果,最终达到想要的效果。
很重要的一点,我们不能依靠工具来作为唯一的判别手段。很多时候,需要在更高一个或者几个层次上优化。举个例子,辛辛苦苦优化某个算法,使得处理器的到最大利用,提高了20%性能,结果发现算法本身复杂度太高了,改进下算法,可能是几倍的提升。还有,在优化之前,自己首先要对数据流要有清楚的认识,然后再用工具来印证这个认识。就像设计前端数字模块,首先要在心里有大致模型,再去用描述语言实现,而不是写完代码综合下看看结果。
小节下,提高传输率的方法有:
缓存对齐,减少访问次数 访存次序重新调度,合并相近地址,提高效率 提高ddr频率减小延迟 使用多控制器提高带宽 使能ddr3的读写命令合并 使能突发模式,让缓存行访问一次完成 指令和数据预取,提高空闲时利用率 在内存带ecc时,使用和内存位宽(比如64位)相同的指令写,否则需要额外的一次读操作 控制器交替访问,比如访问第一个64位数据放在第一个内存控制器,第二个放在第二个控制器,这样就可以错开。 物理地址哈希化,防止ddr反复打开关闭过多bank。 还有个终极杀招,计算物理地址,把相关数据结构放在ddr同物理页内,减少ddr传输3个关键步骤(行选择,命令,预充电)中第1,3步出现的概率
ARM攒机指南-架构篇
捋顺了芯片的基础知识,现在终于可以开始攒机了。
首先,我们跑去ARM,问它有没有现成的系统。ARM说有啊,A73/G71/视频/显示/ISP/总线/系统控制/内存控制器/Trustzone全都帮你集成好了,CPU和GPU后端也做了,还是16nm的,包你性能和功耗不出问题。然后我们再跑到Synopsys或者Cadence买EDA工具,把仿真平台也一起打包了,顺带捎上周边IP和PHY。至于基带,Wifi和蓝牙,先不做吧,毕竟是第一次攒,要求不能太高。
在掏了几亿银子出来后,我们拿到一个系统框图:
A73MP4@3Ghz,A53MP4@1.6Ghz,G71MP8@850Mhz,显示4K分辨率双路输出,视频4K,支持VR,支持Trustzone,内存带宽25.6GB/s(LPDDR4x64@3200Gbps)。
本来可以乐呵呵的一手交钱一手交货的,可我们是来攒机的,不是买整机,我们得知道这个系统到底是怎么搭起来的,免得被坑。于是ARM给了一个更详细的图:
有了这张图,我们就可以对每个模块的性能,功耗,面积还有系统性能进行详细分析了。
首先是大核模块,这是一个手机芯片最受人关注的焦点。我们这里A73MP4的PPA如下:
工艺:TSMC16FFLL+
频率:2.1GHz@SSG/0.72C/0C, 2.5GHz@TT/0.8V/85C,3GHz@TT/1.0C/85C
MP4静态功耗:251mW@TT/0.8V/85C
动态功耗:200mW/Ghz@TT/0.8V/85C
面积:6.7mm,MP4, 64KB L1D, 2MB L2, No ECC, with NEON&FP
跑分:835/Ghz SPECINT2K
也就是说,跑在极限3Ghz的时候,动态功耗是200x1.25x1.25x3=937mW,四核得上4W,而手机SoC最多也就能稳定跑在3W。跑在2.5G时候,动态功耗是200x2.5=500mW,MP4总功耗2.25W,可以接受。
A53的如下:
工艺:TSMC16FFLL+
频率:1.5GHz@SSG/0.72C/0C, 1.6GHz@TT/0.8V/85C
MP4静态功耗:63mW@TT/0.8V/85C
动态功耗:88mW/Ghz@TT/0.8V/85C
面积:3.9mm,MP4, 32KB L1D, 1MB L2, No ECC, with NEON&FP
跑分:500/Ghz SPECINT2K
四核跑在1.6Ghz时功耗463毫瓦,加上A73MP4,一共2.7瓦。
这里的缓存大小是可以配置的,面积越大,性能收益其实是递减的,可以根据需要自行选取。
A53的:
A73的:
作为CPU,集成到SoC中的时候,一个重要的参数是访存延迟。有个数据,在A73上,每减少5ns的访存时间,SPECINT2K分数就可以提高1%。总的延迟如下:
上图只是一个例子,频率和参考设计并不一样。在上图,我们可以算出,访存在CPU之外,也就是总线和DDR上总共需要58.7ns,这个时间也就是静态延时,在做系统设计时可以控制。要缩短它,一个是提高时钟频率,一个是减少路径上的模块。在ARM给的系统中总线使用了CCI550,如果跑在1Ghz,一个Shareable的传输从进去到DDR打个来回需要10cycle,也就是10ns(上图是CCI500跑在800Mhz,所以是12.5ns)。CCI550和CPU之间还需要一个异步桥,来隔绝时钟,电源和电压域。这个异步桥其实也很花时间,来回需要7.5ns,6个cycle,快赶上总线延迟了。但是没办法,它是省不掉的,只有通过增加总线频率来减少绝对延迟。只有一种情况例外,就是异步桥连接的两端时钟频率是整数倍,比如这里的内存控制器和CCI之间频率相同,那可以去掉。
有的设计会把CCI550以及它上面的CPU,GPU作为一个子网挂在NoC下,由NoC连到内存控制器。这样的好处是可以把交织和调度交给NoC,坏处是凭空增加额外一层总线10-20ns的延迟,CPU的跑分就要低了2-4%。在ARM的方案中,交织由CCI直接完成,而调度交给内存控制器。既然所有主设备的访问都需要到DDR,那由DMC来调度更合适。再加上没有采用两层总线的链接方式,一共可以去掉两层异步桥,省掉12个cycle,已经快占到整个通路静态延迟的五分之一了。所以,现在我看到的主流总线拓扑,都是把CCI直接连内存控制器。那可不可以直接把CPU连内存控制器?只要DMC的端口够多,也没什么不可以,但是这样一来大小核就不能形成硬件支持的SMP了,功能上不允许。
路径上还有DMC和PHY的延迟,也是将近15ns,20cycle,这部分挺难降低。如果实现Trustzone,还要加上DMC和CCI之间的TZC400延迟,还会再多几个cycle。至于DDR颗粒间的延迟(行选择,命令和预充电),可以通过准确的DMC预测和调度来减少,稍后再讲。
静态延迟算完,我们来看带宽和动态延迟。
CCI和CPU/GPU之间是ACE口相连,数据宽度读写各128bit,16Byte。如果总线跑在800Mhz,单口的理论读或者写带宽分别是10.8GB/s。这个速度够吗?可以通过CPU端和总线端的Outstanding
Transaction个数来判断。A73的手册上明确指出,ACE接口同时支持48个Cacheable的读请求,14x4(CPU核数量)设备和non-Cacheable的TLB/指令读,7x4+16写,这是固定的。而对应的CCI550的ACE接口支持OT数量是可配置的,配多大?有个简单公式可以计算。假设从CPU出来的传输全都是64字节的(Cacheable的一定是,而non-Cacheable的未必),而我们的带宽是10.8GB/s(假设是读),而一个传输从进入ACE到离开是51.3ns(按照上图计算),那么最大OT数是10.8x51.3/64=8.也就是说,只要8个OT就可以应付CPU那里100多个读的OT,多了也没用,因为总线数据传输率在那。从这点看,瓶颈在总线的频率。
那为什么不通过增加总线的数据位宽来移除这个瓶颈呢?主要是是位宽增加,总线的最大频率相应的会降低,这是物理特性。同时我们可能会想,真的有需要做到更高的带宽吗,CPU那里发的出来吗?我们可以计算一下。先看单个A73核,A73是个乱序CPU,它的读来自于三类方式,一个是读指令本身,一个是PLD指令,一个是一级缓存的预取。A73的一级缓存支持8路预取,每路预取范围+/-32,深度是4,预取请求会送到PLD单元一起执行,如下图。所以他们同时受到BIU上Cacheable
Linefill数目的限制,也就是8.此外还有一个隐含的瓶颈,就是A73的LSU有12个槽,读写共享,所有的读写请求(包括读写指令,PLD和反馈过来的预取)都挂在这12个槽中独立维护。
言归正传,假设系统里全是读请求,地址连续;此时A73每一条读指令的延迟是3个cycle(PLD可能只需要1个cycle,我不确定)跑在3Ghz的A73,一每秒可以发出1G次读,每个8字节(LDM)的话,就是8GB/s带宽,相当于每秒128M次Linefill请求,除以BIU的8个Linefill数量,也就是每个请求可以完成的时间是60ns这和我们看到的系统延迟接近。此时一级缓存预取也会起作用,所以8个Linefill肯定用满,瓶颈其实还是在在BIU的Linefill数量,按照50ns延迟算,差不多是10GB/s。假设都是写,指令一个周期可以完成,每cycle写8字节(STM),带宽24GB/s。如果写入的地址随机,会引起大量的Linefill,于是又回到BIU的Linefill瓶颈。地址连续的话,会触发Streaming模式,在BIU内部只有一个传输ID号,由于有竞争,肯定是按次序的,所以显然这里会成为写的瓶颈。由于写的确认来自于二级缓存,而不是DDR(是cacheable的数据,只是没分配一级缓存),延迟较小,假设是8个cycle,每秒可以往外写64B/8=8GB的数据。总的来说,A73核内部,瓶颈在BIU。
到了二级缓存这,预取可以有27次。当一级缓存的BIU发现自己到了4次的极限,它可以告诉二级缓存去抓取(Hint)。单个核的时候,4+27<48,瓶颈是在一级缓存的BIU;如果四个核同时发多路请求,那就是4x8=32个OT,再加上每路都可以请求二级缓存,其总和大于ACE的48个Cacheable读请求。按照带宽算,每核是10GB/s,然后CPU的ACE口的读是48*64B/50ns=60GB/s。虽然60GB/s大于4x10GB/s,看上去像是用不满,但是一级缓存会提示二级缓存自动去预取,所以,综合一级二级缓存,簇内的瓶颈还是在ACE接口上,也意味着总线和核之间的瓶颈在总线带宽上,并且60GB/s远高于10.8GB/s。
A53是顺序执行的,读取数据支持miss-under-miss,没有A73上槽位的设计,就不具体分析了。
这里我简单计算下各个模块需要的带宽。
CPU簇x2,每个接口理论上总线接口共提供读写43.2GB/s带宽(我没有SPECINT2K时的带宽统计)。
G71MP8在跑Manhattan时每一帧需要370M带宽(读加写未压缩时),850Mhz可以跑到45帧,接近17GB/s,压缩后需要12GB/s。
4K显示模块需要4096x2160x4(Bytes)x60(帧)x4(图层)的输入,未压缩,共需要8GB/s的带宽,压缩后可能可以做到5GB/s。这还都是单向输入的,没有计算反馈给其他模块的。
视频需求带宽少些,解压后是2GB/s,加上解压过程当中对参考帧的访问,压缩后算2GB/s。
还有ISP的,拍摄视频时会大些,算2GB/s。
当然,以上这些模块不会全都同时运行,复杂的场景下,视频播放,GPU渲染,CPU跑驱动,显示模块也在工作,带宽需求可以达到20GB/s。
而在参考设计里,我们的内存控制器物理极限是3.2Gbpsx8Bytes=25.6GB/s,这还只是理论值,有些场景下系统设计不当,带宽利用率只有70%,那只能提供18GB/s的带宽了,显然不够。
而我们也不能无限制的增加内存控制器和内存通道,一个是内存颗粒成本高,还有一个原因是功耗会随之上升。光是内存控制和PHY,其功耗就可能超过1瓦。所以我们必须走提高带宽利用率的路线。
在这个前提下,我们需要分析下每个模块数据流的特征。
对于CPU,先要保证它的延迟,然后再是带宽。
对于GPU,需要保证它的带宽,然后再优化延迟,低延迟对性能跑分也是有提高的。
对于视频和ISP,带宽相对来说不大,我们要保证它的实时性,间接的需要保证带宽。
对于显示模块,它的实时性要求更高,相对于应用跑的慢,跳屏可能更让人难以容忍。
其余的模块可以相对放在靠后的位置考虑。
在CCI550上,每个端口的带宽是读写共21.6GB/s,大小核簇各需要一个端口,GPU每四个核也需要一个端口。显示和视频并没有放到CCI550,原因稍后解释。CCI550的结构如下:
它一共可以有7个ACE/ACE-Lite进口,读写通道分开,地址共用,并且会进行竞争检查,每cycle可以仲裁2个地址请求。之前我们只计算了独立的读写通道带宽,那共用的地址会是瓶颈吗?算一下就知道。对于CPU和GPU,所有从端口出来的传输,无论是不是Cacheable的,都不超过64字节,也就是16x4的突发。一拍地址对应四拍数据,那就是可以同时有八个端口发起传输,或者4个通道同时发起读和写。假设在最差情况下,CPU+GPU同时发起shareable读请求,并且,读回去的数据全都引起了eviction,造成同等数量的写。此时数据通道上不冲突,地址通道也正好符合。如果是CPU+GPU同时发起shareable写请求,并且全都命中别人的缓存,引起invalidate,读通道此时空闲,但是invalidate占用地址,造成双倍地址请求,也符合上限。到DMC的地址不会成为瓶颈,因为共四个出口,每周期可以出4个。这里,我们使用的都是shareable传输,每次都会去Snoop Filter查找,每次可能需要两个对TAG
RAM的访问(一次判断,一次更新标志位,比如Invalidate),那就是每cycle四次(地址x2)。而我们的Tagram用的是2cycle访问延迟的2块ram,也就是说,需要同时支持8个OT。这一点,CCI550已经做到了。在以上的计算中,DMC是假设为固定延迟,并且OT足够,不成为瓶颈。实际情况中不会这么理想,光是带宽就不可能满足。
在CCI550中,有两处OT需要计算,一个是入口,每个端口独立,一个是做Snooping的时候,所有通道放在一起。之前我们计算过,如果静态延迟是50ns,单口的读需要8个OT。但是,上面的静态延迟算的是DDR命中一个打开的bank,并且也不需要预充电。如果加上这两步,延迟就需要85ns,而CCI必须为最差的情况作考虑。这样,OT就变成了10.8x85/64= 15。写的话需要的更少,因为写无需等待数据返回,只需要DMC给Early
response就可以,也就是穿过CCI550(10cycle)加上少于DMC的静态延迟(10cycle),不超过20ns,OT算3。所以,单口OT就是18,意味着需要4x18=72个来存储请求(四个入口)。
在出口,如果是shareable的传输,会需要去查表实现snooping,此时所有的请求会放在一起,它的大小按照四个出口,每个带宽2x10.8GB/s,共86.4GB/s。但是,由于我们DDR最多也就25.6GB/s,计算OT时候延迟按照读的75ns(85-10,CCI本身的延迟可以去掉了,写延迟小于读,按照读来计算),75x25.6/64=30。如果是non-shareable传输,那就还是使用入口的OT。
上面计算出的OT都是用来存储读写请求的,其数据是要暂存于CCI550内部的buffer。对于写来说如果数据不能暂存,那就没法给上一级early
response,减少上一层的OT;对于读,可以在乱序地址访问时,提高效率。由于DMC会做一定程度的调度,所以CCI550发送到DMC的读写请求,很多时候不是按照读请求的次序完成的,需要额外的缓冲来存储先返回的数据。下面是读buffer大小和带宽的关系图。我们可以看到,对于随机地址,buffer越深带宽越高。而对于顺序地址访问,几乎没有影响。这个深度根据队列原理是可以算出来的,我们这里就不写了。
同样,增加缓冲也可以减少总线动态延迟,下图一目了然:
至于设置缓冲的大小,根据队列原理,由于受到DMC调度的影响,数据回到CCI500的延迟期望值不容易计算,而直接设比较大的值又有些浪费(面积相对请求缓冲较大),比较靠谱的方法是跑仿真。
关于CCI550的资源配置,还有最后一项,即tag ram的大小。CCI550的特点就是把各个主设备内部的缓存标志位记录下来提供给Snooping操作,那这个tag ram的大小该如何定。理论上,最大的配置是等同于各级exclusive缓存的tag ram总合。如果小于这个值,就需要一个操作,叫做back invalidation,通知相应的缓存,把它内部的标志位去掉,免得标志位不一致。这个是由于tag ram本身的大小限制引入的操作,在执行这个操作过程中,同地址是有竞争的,这个竞争是多余的,并且会阻塞所有缓存对这一地址的snooping操作,所以需要尽量避免。ARM定义了一个CCI550中的tag ram和原始大小的比例,使得back invalidation保持在1-2%以下,参考下图:
简单来说,可以用所有exclusive cache总大小的10%,把back
invalidation限制在1%以下。
其他的配置参数还有地址线宽度,决定了物理地址的范围,这个很容易理解。还有传输ID的宽度,太小的话没法支持足够的OT。
到这里为止,我们已经设置好了硬件资源的大小,尽量使得动态延迟接近静态延迟。下图是全速运行时的带宽和功耗(这里移除了内存的瓶颈)。
在这里,读写比例是2:1,也就是说出口收到的请求最多64+32=96GB/s。但我们可以看到有时是可以做到超越100GB/s的,为什么呢?因为有相当部分是命中了内部的tag,转而从缓存进行读操作。我们之前算过,Snooping操作在CCI550内部访问tag ram是足够的,但是如果命中,就需要从别人的缓存读数据,而这就收到上级缓存访问窗口的限制。所以我们看到当命中率是100%,总带宽反而大大下降,此时的瓶颈在访问上级缓存。所幸的是,对于SMP的CPU,通常命中率都在10%以下,正处于带宽最高的那段。至于通用计算,是有可能超过10%的,等到GPU之后再讨论。
第二副图显示了动态功耗,在25GB/s时为110毫瓦,还是远小于CPU的功耗,可以接受。静态功耗通常是10-20毫瓦,对于待机功耗其实并不低,而且tag
ram是没法关闭的(300KB左右),只要有一个小核在运作,就必须打开。所以CCI550的静态功耗会比CCI400高一些,这就是性能的代价,需要有部分关闭tag
ram之类的设计来优化。
不过,一个没法避免的事实是,总线的入口带宽是86.4GB/s,出口却只能是25.6GB/s。我们需要一些方法来来达到理论带宽。在总线上,可以使用的方法有调度和交织。调度之前已经提到过,使用乱序来提高不存在竞争问题的读写传输,CCI550自已有内嵌策略,不需要配置。
而交织在之前的文章中也提过。 在CCI550上,只接了CPU和GPU,而他们发出的所有传输都不会超过64字节,也就是一个缓存行长度。只有一种可能的例外,就是exclusive传输,在spin lock中会用到。如果有一个变量跨缓存行,那么就有可能产生一个128字节的传输。而由于这类可能存在的传输,CCI550可以设置的最小粒度是128字节,来避免它被拆开到两个内存控制器,破坏原子性。不过到目前为止,CPU/GPU最多发出64字节的传输,不会有128字节,如果有跨缓存行的变量,直接切断并报异常。这样虽然会造成软件逻辑上的错误,但这错误是软件不遵守规范引起的,ARM软件规范里明确禁止了这类问题。
对于视频和显示模块,它们并没有连在CCI550。因为CCI550内部只有两个优先级,不利于QoS。QoS我们稍后再讨论,先看系统中的NIC400总线。参考设计中NIC400链接了视频/显示和DMC,但其实NIC400并没有高效的交织功能,它的交织必须把整个物理地址空间按照4KB的页一个个写到配置文件。此外,它也没有调度和缓冲功能,所以在复杂系统中并不推荐使用。NIC400更适合简单的系统,比如只有一个内存控制器,可以用更低的功耗和面积实现互联。在这里我们可以使用NoC来替换。NoC还有一个适合传输长数据的功能,就是分割,可以把265字节甚至4KB的突发传输隔成小块,更利于交织。不过分割需要注意,必须把传输的标识符改了,并且要自己维护,并维持数据完整性。
这里就引出了一个问题,到底我们交织的粒度是多少合适?粒度越大,越不容易分散到各个DMC,而粒度越小,越不容易形成对某个DMC和DDR Bank的连续访问。
在四个通道的DMC上,CCI550使用了这样一个哈希变换,来使得各个模块的传输能够平均分布:
Channel_Sel[0] = Addr[8] ^ Addr[10] ^ Addr [13] ^ Addr [21]
Channel_Sel[1] = Addr[9] ^ Addr [11] ^ Addr [16] ^ Addr[29]
然后在DMC,使用了这样的变化来使访问分散到各个Bank:
For four channel memory: Addr[29:0] = {Addr[31:10], Addr[7:0]}
经过哈希化后,在CPU的顺序地址上效果如下:
在显示模块的上效果如下:
显然,在256字节的交织颗粒时效果最好,尤其是对于连续地址。对于随机地址,有个细节我们可以注意下,在颗粒度64/128字节的时候最好,这其实也符合预期。并且,由此还可以引出一个新的优化:对于随机访问多的地址段,我们使用128字节交织,而对于顺序访问多的地址段,使用256字节交织。但是请注意,这个编码必须对所有的主设备都保持一致,否则一定会出现数据一致性或者死锁问题。
刚刚我们提到了在DMC中的哈希优化。DMC最重要的功能是调度,这是提高带宽利用率的关键。ARM的DMC可以做到在多个主设备的64字节随机地址访问时做到94%的利用率(读),写也可以到88%。我们可以通过一些参数的设置来影响它的调度判断:读写操作切换时间,Rank(CS信号)切换时间以及页内连续命中切换阈值,高优先超时切换时间等,这些看名字就能理解,不详细解释了。
调整完这么多参数,还有一个最根本的问题没有解决:DDR理论带宽也只有25.6GB/s,各个主设备一起访问,总会有拥挤的时候。这时该采取什么策略来最大程度上保证传输?答案是QoS。QoS的基本策略是设置优先级,其次还有一些辅助策略,比如动态优先级调整,整流等。
上图是优先等级表,左边是总线的(包括CCI和NoC/NIC),高低依次是显示模块>视频>CPU>GPU>PCIe>DMA,基带跳过。在CCI550内部,实际上只有高和第两种优先级,CPU高,GPU低,没法区分更多的主设备。CCI550的优先级高低是给外部用的,可以拿来给DMC,让DMC定一个阈值,告诉CCI哪些级别之上优先发送,哪些暂缓。
当中的表是DMC内部对QoS优先级的重映射。DMC500支持把某个模块的优先级提高,并动态调整。这里,CPU在每秒发送1.6GB之后,优先级会被降低,和外部总线一样。下图中,我们可以看到在没有QoS的时候,CPU的延迟最大(最左边noqos),有了QoS,由于CPU的优先级相对较高,延迟大大降低(qos_only)。而用了重映射之后,在1.6GB之下,延迟又进一步较大降低。
让我们再看一个例子,更好的说明如何通过调整优先级来在有限的带宽下保证实时性:
在上图,我们看最右边,由于显示和视频总带宽需求不高,所以都能满足。
而这两张图里面,带宽需求上升,我们看到Video的带宽反而下降,而CPU不变。可在我们设置的表里,CPU的优先级是低于视频模块的。于是我们利用DMC的门限,告诉CCI不要送低优先级的,并把CPU中小核的优先级降低到门限之下,这样只要有视频流,CCI送到DMC的数据就会减少,如下图:
当然,我们也可以通过别的机制来达到类似的效果,比如限流等。攒机是个细致活,功夫够了肯定能调好,就是需要耐心和积累。
ARM攒机指南-安全篇
Trustzone可以追溯到十多年前,ARMv7公布的时候就有了,可惜一直没有什么实际应用。直到近几年开始,才真正的有厂商开始把这个方案大规模用于芯片里。它的基本设计思想是用硬件防护来弥补软件的漏洞。目前看到的主要有五个应用领域:
第一是支付。知乎上有篇文章把支付过程中的利益链分析的非常清楚:为什么 NFC 到目前为止仍然不温不火?简单来说,一方以运营商和银联为代表,用运营商的SIM卡作切入点,支付经POS机走到银联;另一方以支付宝和微信支付为代表,用他们的手机应用作切入点,支付经过互联网公司到银行。这两个阵营各有一帮小弟摇旗呐喊,好不热闹。而Trustzone也有两个分支:ARMv8-M Trustzone和ARMv8-A Trustzone,分别适用于SIM卡和手机支付。不幸的是,ARMv8-M Trustzone完全不适合中国,原因很简单,运营商要求SIM卡的成本要做到6分钱,而完整的实现ARMv8-M Trustzone需要5到10倍于常规MCU的面积,就算使用180nm的工艺都做不下来,更不用说版税和别的费用了,所以SIM卡这条路已经堵死。对于手机支付,三星,联发科,海思和展讯早已在芯片上集成了ARMv8-A Trustzone,为支付提供了硬件支持。但是目前的支付宝和微信支付仍然以软件方案为主,只有在使用指纹支付时才会用到Trustzone,普及基于硬件的安全支付还有一段路要走。而苹果的Apply Pay剑走偏锋,它以NFC卡为切入点,交易走的是POS机,但同时也使用了类似Trustzone的技术来保护密钥和指纹。总而言之,技术不是重点,两种支付方式都可以拿Trustzone来实现,关键是怎么站队。
同样是在手机上,最新的安卓7.0要求必须对关键数据进行保护,包括密钥,指纹等信息。虽然谷歌并没有明确必须用硬件来做防护,但是他对于基于软件虚拟机等方案是持保留态度的。所以要上新版本安卓的小伙伴们千万要注意提早规划,绕过这个坑。
第二是DRM,数字版权管理,也就是正版内容保护。如果用户要在手机上看最新好莱坞大片,那么播放设备必须符合一个标准,这个标准可以用Trustzone来实现。国内已经在积极的推动这个事情,也就是ChinaDRM,估计2017年就有标准出来了。需要强调的是,使用Trustzone实现DRM,本身并不限制盗版,只不过让手机多了一个功能,有了一个看好莱坞大片的渠道。如果不需要,完全可以关闭,对用户没有任何影响。目前在手机的芯片成本上,支持DRM增加的面积并不大,三星,联发科,海思和展讯已经早早的集成了DRM支持了。在国外的机顶盒芯片上,对于版权保护早要求,也早就有方案和认证,有些甚至比目前的Trustzone要求更高。相信随着ARM处理器在机顶盒上的普及,Trustzone会逐渐完善。
第三是无人机芯片,大疆已经走在了最前面,第二名连影子都没看见。无人机上几大应用,图像传输,图像处理,识别,飞控,存储,每一块都有安全的诉求。利用Trustzone可以做到,在芯片里流动的数据,包括视频和应用软件信息,每一步都在安全系统的控制之下,哪怕飞机被人抢去,都需要极大的代价才能拿到闪存以及内存里面的数据。如果以后上安卓或者其他操作系统,哪怕软件系统被黑客攻破,数据和控制还是安全的。最后,如果国家或者行业出台政策,要求实施禁飞区,那么哪怕无人机的主人自己去修改闪存和软件,都可以被强制接管。这些功能必须在芯片设计阶段就考虑到。
第四是物联网。物联网的安全有好几种做法,可以把安全检测放在服务器端或者末端芯片上。末端通常是一个MCU加上传感器和互联模块,面积较小。用硬件trustzone实现的话,加解密和密钥管理等功能会需要额外内模块,可能比MCU本身都大,成本太高。但如果是附加值高的芯片就没什么问题。ARM在2016年专门为物联网发布了MCU上的ARMv8-M Trustzone,用相对Cortex-A系列更低的成本来实现安全。
第五是汽车。在这个领域,ARM在R系列芯片上使用了和手机Trustzone截然不同的技术-虚拟机来实现ARMv8-R Trustzone,在安全的同时兼顾实时性。瑞萨和高通(前NXP)已经在往这条路上探索,国内好像还没听到什么公司有动作。
其他还有一些企业应用的领域,比如存储,一般使用TCG标准。这个标准定义了存储控制器内部和服务器端的加密格式,而这蕴含了安全启动的需求。同时,整个数据流在存储侧全程都是加密的。这些都可以用Trustzone的相应功能来实现。目前有人已经做了实现,不过ARM并没有提供现成的方案,还是需要芯片公司自己软硬件定制。
在服务器上,Intel有基于SGX的防护,而ARM并没有正式提出服务器的安全解决方案,给客户推荐的也不过是照抄手机Trustzone。我看了下SGX的官方PPT, https://software.intel.com/sites/default/files/332680-002.pdf,它是将合法软件的安全操作封装在一个容器中,保护其不受恶意软件的攻击,特权或者非特权的软件都无法访问容器,也就是说,一旦软件和数据位于容器中,即便操作系统或者和VMM(Hypervisor)被攻破,也无法影响容器里面的代码和数据。硬件实现是在正常的MMU之后又加了一层,专门检测是不是属于容器的,同时也会防止Snooping等操作的窥探。设置这个容器的寄存器,指令和独立MMU游离于Hypervisor之外,不能被更高优先级所修改。想法是很好,但SGX仅仅是CPU侧的防护,还是可以通过PCIe和DMA设备直接读取DDR内容的。ARMv9里面也会有类似设计,同时注意所有系统主设备的访问,最后会有介绍。
纵观以上各个领域,Trustzone做了很多在设备端的硬件安全保护,但请注意,Trustzone并不是一个服务器和客户端的完整交互方案,也没有规定密钥的交互规范,对于支付和DRM,还是需要应用层来共同解决。
接下来让我们从技术层面来定义Trustzone到底能做什么:
1.防止操作系统被攻破后关键数据泄密,关键数据存放在特定内存区域,而那块区域,只有安全操作系统才有可能读到。
2.防止通过JTAG等调试接口读到寄存器,缓存,内存或者闪存数据。
3.从芯片制造开始,最初的密钥可以用芯片熔丝实现,往后启动的每一步都需要最高特权级和密钥验证,建立信任链,杜绝软件被替换或者被恶意读取。
4.防止边带攻击,比如量取内存颗粒的信号猜测数据,制造故障让检验模块停止工作,替换外围器件,输入特定数据确定电磁信号特征,打开芯片直接量内部信号线等。
上图是一个典型的ARM SoC内部结构,在这个结构里,Trustzone做的事情是保护数据在芯片内部的安全,不允许非授权的访问,哪怕这个访问来自CPU。初看有些复杂,不过我们可以拆开慢慢分析,从硬件角度开始比软件更清楚。
首先,按照Trustzone的划分,一个芯片内被划分为安全世界和非安全世界。上图中,中间黑色的部分是总线,总线上面是主设备,下面是从设备(主设备中的缓存是例外,这个以后说)。读写请求总是从主设备发往从设备的。
作为从设备,区分它是不是属于安全世界相对简单。如果一个从设备不存在成块的空间映射,比如I2C或者PWM,那么我只要在总线访问它的时候,额外的加入一个管脚(取名为PROT),就可以告诉它本次访问是不是来自安全世界。如果从设备本身是完全属于被保护的安全世界,不接受非安全的访问,那么只要简单的拒绝,返回错误或者无意义数据即可。同样,如果从设备本身处于非安全世界,那么对于安全和非安全访问,都可以返回正确数据。还有,从设备所处于的世界,是可以动态配置的,且动态配置本身需要处在安全世界,这个以后讨论。
对于块设备,包括闪存,sram和内存等,它们的某些地址块需要处于安全世界,其他的处于非安全世界。为了实现这一点,就需要在它们前面插入一个检验模块(例如图中左方,DDR上面的TZC400),来判断某个地址是不是能被访问。当地址被送到这个检验模块,模块会结合PROT管脚去查表,看看本次访问是不是被允许,然后做相应措施。表本身和之前的动态配置一样,必须是在安全世界里面配置的。
至此,从设备就分析完了,是不是感觉特别简单?还有些细节,在把主设备也讲完后,我们会从系统角度来关注。
对于一般主设备,不考虑自带的缓存时,其实和从设备也差不多,也分为安全和非安全,可以在安全世界动态配置。配置完成后,这些主设备会按照自己所处的世界,驱动PROT管脚和地址来访问从设备,得到相应返回。不过这里的一般主设备不包括中断控制器,系统MMU,调试模块和处理器,接下来对这些例外模块进行具体分析。
首先是处理器。在上图情况,接了CCI总线后,处理器接在缓存一致性端口ACE上(不明白的请参考以前的文章),它的缓存是可以被别人访问的,并且这个访问,是从主设备到主设备(当然,在处理器内部是从端口),不会经过总线送到内存,也不会经过检验模块TZC400。这时就有个漏洞,通过操纵一个非安全世界的模块,比如上图的橙色主设备,假装去读一个被安全世界保护的内存地址。这个地址本来存在于内存,被TZC400保护,可是由于总线的监听功能,读请求有可能被发往处理器缓存,从而绕过保护。为了防止这种情况,处理器在所有的页表和缓存都做了特殊设计,加了一个标志位,标志本缓存行是否属于安全世界。如果别的非安全世界主设备来监听安全世界缓存行,由于安全位不同,处理器会认为这是两个不同地址,哪怕它们的地址一致,返回缓存未命中。这样,就不会把数据泄漏。有人会问,这个标志位来源于页表,改了页表中的这一位不就可以访问了?其实不行。因为安全世界页表位于被保护的内存区域或者缓存,就算破解了操作系统也无法访问。又有人会说,那改了非安全世界的页表中安全位,并伪造一个安全世界的地址,岂不是可以让CPU模拟出一个访问安全世界的传输,送到总线和TZC400?TZC400或者对端缓存一看地址和PROT管脚都是符合要求的,应该就会返回保密数据吧?想法是不错,可是当CPU位于非安全世界时,它会忽略页表中的安全位,所以不可能发出PROT为安全的传输。所以,我们可以对这点放心。
以上是别的主设备访问处理器,那如果处理器本身处于非安全世界,有没有可能访问其他主设备的安全缓存?当然有。所以不要把其他主设备接到ACE端口,以免被监听,一般主设备是不会做缓存上的安全与非安全区分的。接到ACE-Lite接口无所谓,反正设计上就无法被读取缓存数据。除此之外,还存在一个例外,就是GPU。在最新的ARM
G71图形处理器上,是支持双向硬件一致性的。也就是说,GPU也可以被监听缓存的。为了简化设计,图形处理器被设成永远处于非安全世界,CPU尽管读,不在乎,它使用另外一种机制来保护数据,以后介绍。
对处理器缓存熟悉的人可能会想到用跨缓存行的非安全变量来访问被保护的数据。没用的,处理器设计者早就想到这点,要不就是非对齐访问异常(包含exclusive access的时候),要不就不会给你数据,具体到每个处理器有所不同。
还有一个漏洞没堵上,那就是缓存维护,TLB和分支预测操作。ACE端口包含了DVM操作来维护它们,安全性如何保障?同样的,地址中也有安全和非安全位。不过话说回来,DVM操作无非就是无效化某些缓存,分支预测和TLB项,不存在安全数据被读取,TLB被篡改的情况。
到这里可能你会觉得有点晕,不少漏洞需要堵。我们可以回顾一下,需要记住的是各种缓存操作,通过安全标志位保护,避免漏洞。对比处理器设计者所要考虑的情况,这点漏洞不值一提。
杜绝了缓存漏洞后,还有别的隐患,比如仿真器。调试模块可以被用来访问各个从设备,也可以访问和影响处理器内部资源。从设备侧的防护很容易,把调试模块当成一般的主设备处理就行。处理器内部的寄存器,缓存等资源,需要处理器从设计开始,就要为所有资源定义安全级别。被保护的资源对于来自调试模块的未授权访问会被禁止。只有通过安全启动链,安全世界的软件才能打开寄存器SDER,从而允许外部仿真器影响被保护的安全世界资源和处理器运行状态,访问被保护的资源。
那处理器内部的资源是怎么划分的?以ARMv8举例,如下图:
这幅图相信很多人都看到过。ARMv8的处理器被分成四个特权等级,通常EL0跑用户态程序,EL1内核,EL2虚拟机。EL0-1分为安全与非安全,EL3只有安全世界,EL2不区分,两个世界的切换必须经过EL3。我们谈到的处理器内部资源,包括寄存器,缓存,异常,MMU,很多都会分组,组之间看不到或者低级不可访问高级,从而保证安全。没有分组的,比如通用寄存器,就需要软件来维护,防止非安全世界的看到安全世界的数据。
引起安全切换的会有几种可能:中断和SMC指令。中断分为如下几种情况:
非安全世界下,在EL1或者EL0,当一个非安全中断来临,那么系统没必要切换安全状态,作为一般中断处理,切到EL1即可。
非安全世界下,在EL1或者EL0,当一个安全中断来临,那么系统必须先切到EL3,不然就没法做安全世界切换。
安全世界下,在EL1或者EL0,当一个安全中断来临,没必要做安全世界切换,作为一般中断处理,切到EL1即可。
安全世界下,在EL1或者EL0,当一个非安全中断来临,那么系统必须先切到EL3,不然就没法做安全世界切换。
当跳到EL3的Secure Monitor程序处理上下文切换时,IRQ/FIQ中断屏蔽位不起作用,哪怕打开了也不会触发,直到Secure
Monitor处理完,向下跳到相应的安全世界EL1时,才会让原来的中断屏蔽恢复,从而触发中断。此时处理中断的是安全世界的中断程序,处于被保护的内存区域,杜绝非安全世界的程序篡改。
那怎样触发安全与非安全中断呢?这在中断控制器里有定义,早年的定义中只有FIQ可以作为安全中断,后期的可配置,并且,相应的安全世界配置寄存器只有在处理器的安全世界中才可以访问。
SMC指令和中断触发类似,只不过软件就可以触发,切换到Secure
Monitor。这里,非安全软件可以提出触发请求,在通用寄存器填入参数,却无法控制安全世界的处理程序做什么,也依然看不到被保护内存数据。所以防止数据泄密的任务就靠安全操作系统了。
至此,安全启动后的基本硬件防护已经完成,但如果你以为这就是Trustzone,那就错了,精彩的在后面。
我们可以把Trustzone放到实际应用里面看看是不是可行。以DRM举例,如下图:
在播放授权 视频的时候,视频流来自网络或者闪存,它们不需要在安全世界,因为数据本身就是加密过的。然后被解密并放到被保护内存,等待解码。上图中,密码保护和解密是通过安全硬件模块Crypto来完成的,这个我们以后再分析,先处理解密完成后的视频流。此时有两种方案:
第一中,非常自然的,可以把所有的过程在安全世界完成,那么图形处理器,视频处理器和显示模块必须都工作在安全世界,能访问安全世界的数据,才能完成工作。可这样就带来一个问题,那就是驱动。我们知道,图形处理器的驱动是非常复杂的,并且手机上只存在Linux和windows下的图形驱动,和OpenGL ES/DirectX配合。而安全世界的操作系统(TEE,Trusted Execution Environment)是完全不兼容的安全系统,甚至有的都不支持SMP, 完全不存在可能性把图形驱动移植上去,也没有任何意义。这样的话,就只能把图形处理器从流程中挖掉,只留下相对简单也不需要生态的视频和显示模块的驱动,工作在安全世界,而GPU的输出送到显示模块,由显示模块进行混合。这是一种可行的方案,也确实有公司这么做。但是从长远看,图形处理器总是会参与到这个过程的,别的不说,只说VR和AR流行以后,要是虚拟个显示屏出来,上面播放视频,然后放在一个虚拟出的房间,那他们之间肯定是要进行互动的,此时显示模块就需要把视频图层送回GPU进行运算。如果GPU不在安全世界,那就会造成泄密。
为了解决上述问题,有了第二种解决方案,称作TZMP1(Trustzone Media Protection 1),引入了保护世界的概念。保护世界工作于非安全世界,这样才能兼容图形驱动。那安全怎么办?它需要添加四根管脚NSAID,类似于安全世界的PROT信号,只不过做了更细的划分,使得GPU/视频/显示模块要访问被保护内存时,预先定义好了权限。而这个权限的设置,也是通过前文的TZC400来实现的,在安全启动链中就完成。CPU的权限通常是0,也就是最低。而显示控制器权限是只读。
这样一来,我们之前的老问题,恶意缓存监听,又回来了。在新的A73和G71加CCI500/550总线系统里,可以支持双向硬件一致性。这意味着GPU也能被监听。这下大家都在非安全世界,缓存里的安全位不起作用,怎么解决?这需要总线的配合。ARM的总线CCI500/550,有一个保护模式,打开后,不光支持上文的NSAID管脚,还可以在监听的时候,把监听传输替换成缓存行无效化命令,直接让目标把相应缓存行无效化。这样一来,数据还是需要从内存读取,保证安全。并且这个过程对软件透明,无需做任何改动。可是此时,辛辛苦苦设计的硬件一致性就完全起不到加速作用了,性能受到影响。好在运行OpenGL ES的时候,GPU是不会发出共享传输的,CPU也不会没事去监听GPU的数据。而下一代的图形接口Vulkan,会开始使用GPU双向一致性,那时候会有影响。还有一点不利的是,如果同时运行OpenCL和DRM,OpenCL也用不上双向硬件一致性,必须重启系统切换到非保护模式才行。
以上两种方案同时也可用于安全支付,只要把密钥和指纹放在安全内存里,让显示模块单独读取一个安全图层就行。此时,安全图层的数据不可能被处理器上的恶意程序读到,无论显示模块是在安全世界还是保护世界。
在实际使用中,现有的TZC400作为内存保护模块,有几个致命的缺陷。第一,它的配置只能在启动时完成,无法动态改变,也就是说,一旦某块内存给了安全世界,就无法再被非安全世界的操作系统使用,哪怕它是空闲的。在4K视频播放时,往往需要分配几百兆内存,还不止一块。如果一直被占着,这对于4GB内存手机来说是个沉重的负担。怎么解决?只能改成动态配置的硬件。
改成动态后,还会遇到第二个问题。TZC400和它的改进版最多只能支持最小颗粒度为2MB的内存块管理。为什么不弄细些呢?很简单,如果设成4KB,和系统页大小一致,那么4GB的物理内存就需要一百万条目来管理。如果做成片上内存,比二级缓存还大,不现实。而做内存映射,就和MMU一样了,经过CPU的MMU后,数据访问还要再穿越一次MMU,延迟显然大。此外,这一层的MMU无法利用一二级缓存放页表,效率极低。如果继续保持2MB的颗粒,那么Linux在分配内存的时候,很难找到连续的2M物理块,因为他需要512块连续4K物理页来拼接。这样,我们很容易就分配失败。这就是TZMP2V1。
再有一个问题,就是安全世界TEE和非安全世界REE的切换性能。由于安全切换牵涉到上下文的保存,通常它所需要的时间是毫秒级的。在DRM中,由于每一帧都需要进入到TEE来进行密钥操作,每秒钟需要进行几十次切换,累积起来就是一个可观的数字。怎么避免?有一个办法,就是使用一个额外的硬件,在ARM的方案里被称作CryptoCell。它可以接受非安全操作系统世界来的命令,在自己的硬件里执行密码操作。整个过程中不会把安全数据区暴露给CPU,只是把操作结果放在指定非安全内存。这样,CPU就不必进行安全世界上下文切换,只是需要休眠或者轮询结果就可以。当然CryptoCell还有许多其他功能,安全启动,密钥管理,全盘加密时候会用上。
既然TZMP2V1有这么多问题,第三种基于虚拟机的方案就出现了。不过这个方案基本上推翻了Trustzone最初的设计意图,我们来看下图:
在这里,作为内存保护的TZC400完全移除,而系统MMU加了进来。内存保护怎么做?靠物理地址重映射。先看处理器。在启动链中,从EL3向EL2跳的过程时,就定义好保护内存,并且EL2,也就是虚拟机的页表存放于保护内存,EL1的安全页也同样放在保护内存。这样,当处理器进入到EL1,哪怕通过篡改EL1非安全页表的安全位,也最终会被映射到它所不能访问的安全内存,从而起到保护作用。同样的,给处于非安全世界的控制器也加上系统MMU,让设备虚拟化,同样可以控制安全。这就是TZMP2V2。有了系统MMU,页表可以做成4KB大小了,也不用担心CPU那里穿越两次MMU。这时候,也不用担心恶意监听缓存,因为所有穿过二级MMU的访问里,安全位都是经过检验的的。
但是,不看别的,光是为设备加入这些系统MMU,就会增加很多面积。还有,光加MMU不够,还要加入系统的三级甚至四级缓存,才能让MMU效率更高,不然延迟太大。当然,如果设备使用的页表并不很多,可以对MMU简化,比如增大最小颗粒度,减少映射范围,直接使用片内内存。这需要系统设计者来做均衡。对于GPU来说,要支持双向一致性,还得考虑让监听传输通过MMU,不然功能就出问题了。所以,ARM在2016年所定义的TZMP2方案,本质上还是TZMP2V1。TZMP2V2需要到2018年后才有可能出现。
那目前作为过渡方案,应该怎么办呢?可以使用TZMP2v1,然后牺牲一点面积,在类似TAC400的内存访问过滤器上,实现64KB的颗粒度,这样4G空间只需要256K条目就可以实现全映射,并使得查表的延迟保持在1ns左右。每一条目2-4bit属性位,总体64-128KB大小,。在分配物理内存时,使用不同优先级,尽量降低分配64KB内存失败的概率。
如果最终使用了TZMP2V2,那么虚拟化就变成了一个切实需求。然后会发现,ARM的中断和设备的虚拟化还不完善。接下来我从硬件角度解释下虚拟化。
说到虚拟化,先要解释系统MMU。
如上图所示,系统MMU其实很简单,就是个二层地址转换。第一层,虚地址到实地址,第二层,实地址到物理地址。请注意,没有第二层转换时,实地址等同于物理地址。这个模块既可以两层都打开,也可以只开一层,看情况而定。
上图比较清楚的显示了一层映射的过程。其中,设备发出的虚地址请求,会先经过TLB,它里面存了以前访问过的页表项,如果有,就直接返回,没有就往下走到第二步table
walk。第二步里,MMU会按照预设的多级基址寄存器,一级级访问到最终页表。如果MMU位于CPU内,那table walk过程中每次访问的基址和表项,都可以存放于缓存中,大大提高效率。如果在设备上,只有内建的TLB表项,后面没有缓存,那未命中TLB的都是访问DDR,效率自然下降。所以CPU和GPU等经常访存的设备,都是自带第一层MMU和缓存。而对于没有内部MMU,切换页表又不是很频繁的设备,比如DMA控制器,可以在下面挂第一层MMU,此时驱动就简单了,直接把应用程序看到的虚地址给DMA的寄存器就行,MMU会自己按照基地址去查找相应页表并翻译,把实地址送到总线。不然,驱动还要自己查找实地址再写入寄存器。
我们前面说过,在TZMP1和TZMP2v1中,内存保护是靠TZC400来完成的。而到了TZMP2v2,取消了TZC400,这时靠虚拟化的二层地址映射。二层映射的过程和一层映射基本一样,不再详述,但是性能问题会被放大。假设在一层中,经过四级基址查到最终页,而在二层中,这每一级的基址查找,又会引入新的四级基址访问。所以至少要经过4x4+4=20次访存,才能确定物理地址。如果没有缓存的帮助,效率会非常低。其他可行的办法是减少基址级数,比如linux只用了三级页表,但即使如此,也需要3x3+3=12次查找。在包含缓存的ARM
CPU上,虚拟机的效率可以做到80%以上。而二层MMU应用于设备实现设备虚拟化的时候,就需要小心设计了。
有了系统MMU,我们就有了全芯片虚拟化的基础。那在对系统性能和成本做完平衡,采取合适的系统MMU设计之后,是不是就可以实现虚拟化,并且靠虚拟化实现安全了?没那么容易,还有其它问题需要考虑。
虚拟化脱胎于仿真器,就是在一个平台上模拟出另一个平台。在指令集相同的时候,没有必要翻译每一条指令,可以让指令直接被硬件执行,这样指令的效率算是得到了解决。当然,对于某些特殊指令和寄存器访问,还是需要hypervisor处理的。接着第二个问题,访存。我们前面解释过,对CPU来说,高效的虚拟化访存,就是让指令高效的经过两层翻译,而不是每次访存都需要触发虚拟机EL2的异常,切到Hypervisor,再得到最终物理地址。这一点在没有缺页异常的时候,ARM的虚拟化也已经做到了,而有缺页异常时还是需要Hypervisor处理。再接着是设备访存虚拟化,有了系统MMU,也可以高效做到。再就是处理器和设备中断虚拟化,如下图:
最高效的虚拟中断处理,是GuestOS直接接受自己的虚拟终端,而不必跳转到Hypervisor再回来。这就需要GICv4之后的中断协议,之前的还都不支持。实现上,必须要求中断控制器能支持多个虚拟中断号和虚拟设备号,否则没法正确的发送中断请求。而要支持这一特性,又需要把描述符放在内存,而不是控制器的内部寄存器,否则片上内存放不下。这又进一步引入了延迟。还有,设置中断处理跳转的寄存器不应该被GuestOS访问,否则会有安全隐患。做系统设计时必须综合考虑这些因素。
高效的指令,访存和中断形成了高效的虚拟机。在实际应用中,这类驱动被称作pass-through device,穿透式设备,是最高效的一种。其余的方式,还有完全虚拟化设备(无需改驱动但效率最低)和半虚拟化设备(需要特殊驱动和Hypervisor沟通)。在网络应用中,如果是跑数据面转发,必须使用穿透模式。据我所知,思科开放的VPP(Vector Packet
Processing)可以在Intel服务器上做到90%以上的非虚拟化性能,并且可以线性提升多和性能。这靠得是把上文所有的虚拟化特性全部用上,外加Stashing等总线传输技术才能做到。而在ARM平台,支持GICv4的IP得等到2018年才有,做成高效的虚拟机并配上AMBA5的总线,处理器,外加成熟的软件,估计得2020年,和Intel还是有不小的差距。
这里可以顺便介绍下ARMv8-R Trustzone,前面我们说过,它使用了了汽车上的虚拟化来做安全,并且保证了实时性。在正常的虚拟化上,由于存在两个阶段的地址转换,涉及到几十次的访存,延迟大是其次,关键是无法保证确定的访问时间。这在汽车应用上是不可接受的。怎么办?非常简单,第一,去掉虚实转换,大家看到的都是一个物理地址空间,提高效率。第二,只提供二个阶段的地址检查(分别属于应用和虚拟机),并限制检查的表项个数,比如16条,那么就不需要去内存里查找,直接命中片上内存。在中断处理上,减少虚拟中断号和设备号的数量,避免访存。几个措施加起来,就能继续保证确定的较短延迟。当然,ARMv8-R
Trustzone本身其实是支持虚地址的,只不过会引入一些延迟,这就需要系统设计者做权衡了。有了ARMv8-R
Trustzone的隔离,就可以在同一个芯片上跑不同的操作系统和第三方应用,而不必担心安全问题。在汽车上,之前的应用是AUTOSAR,虚拟化要取代它,还有很长的路要走。好在汽车芯片商,比如瑞萨和NXP,对这一转变非常积极,已经开始芯片的设计了。
再来说说ARMv8-M Trustzone。事实上,在2016年前,是不存在Cortex-M上的Trustzone的。很多号称实现了Turstzone的MCU,只是借用了这个概念,其实在安全设计上是有问题的。
在上图中,使用了ARM的核Cortex-M3和硬件安全IP
CryptoCell,APB总线做了PROT位,还在SRAM/Flash和其他所有控制器上加入了地址安全检查,并启用了安全启动链。这样就是符合Trustzone的系统了吗?答案是否定的。这个系统有个致命弱点,如果第三方程序恶意访问机密数据,由于MCU无法区分安全和非安全状态,导致给总线的信号无法实时驱动PROT管脚,从而从设备无法判断到底是不是安全访问。当然,也可以通过软件来拉PROT信号,可是所有程序都是在一个状态下,看到的寄存器都是一致的,恶意程序也能驱动PROT管脚,保护措施就失去了意义。
那难道必须使用ARMv8-M核才能在MCU上做Trustzone吗?也不是。如果在上述系统中,再加一个M3,把它的PROT管脚拉成非安全状态,并把原来M3的PROT管脚拉成安全状态就可以了。第三方应用跑在非安全核,安全应用跑在安全核,他们之间通过硬件mailbox做通讯。并且由于不存在数据缓存,总线也不支持Snooping,也就不存在上文的缓存一致性安全问题。
那真正的ARMv8-M Trustzone是什么样的?如下图:
这里使用了新的M33核,它内部可以区分安全状态,所以就不需要两个核。并且AHB5总线本身就支持PROT管脚,配合CryptoCell IP,就可以实现类似A系列的安全启动和访问了。在M33上,除了实时性之外,还需要把安全部分的硬件尽量做小,否则MCU面积成本太高,不会有人使用。ARM使用了额外的硬件逻辑来帮助定义安全,如下图所示,需要设计者自己顶一个很小的硬件映射表Arbitration Unit,来定义哪些区域是安全的。这样牺牲了灵活性,却省了面积。同时由于没有缓存,也不需要缓存中加入额外的面积帮助判断。
最后,再阐述一下安全启动机制。之前的Trustzone的工作,都是在保证运行时不被恶意程序窃取安全数据。那如何保证系统从启动开始,所有的系统软件都没有被恶意篡改?前面我们提到过芯片制造过程中,用熔丝fuse实现一些特殊的比特位,这些熔丝一旦被写入,就再也无法更改。这一机制可以被用来写入公钥。当然,由于熔丝的成本较高,我们可以只写入公钥的哈希值,而把公钥存于芯片内部的Rom或者片外闪存。启动的时候,熔丝里哈希后的公钥,可以和片外的公钥做对比,确保哈希值未被篡改。然后,再使用这个公钥,去验证所有的启动代码的签名。如果有些启动代码本身需要保密,不被人读懂,可以用对称加密算法加密后,再对对称算法的密钥做签名和验证。这样一直启动到EL3,就可以建立信任链,为Trustzone打下基础。
不过还是有个问题没解决,那就是如何防止设备本身的身份验证问题。如果服务端需要确认某个设备是不是一个可信任节点,就需要设备用非对称算法的私钥对特征字段进行签名,然后发送到服务端。这个过程有可能被物理攻击,从而泄露私钥,芯片本身也有可能被磨片,造成私钥泄露。这时,可以用闪存的RPMB分区保护数据存储,用全盘加密保护传输,或者干脆把私钥存于另外的设备,从需求和成本达成一个平衡。
ARM攒机指南-后端篇
工作中经常遇到和做市场和芯片同事讨论PPA。这时,后端会拿出这样一个表格:
上图是一个A53的后端实现结果,节点是TSMC16FFLL+,数据经过改动,并不是准确结果。我们就此来解读下。
首先,我们需要知道,作为一个有理想的手机芯片公司,可以选择的工厂并不多,台积电(TSMC),联电(UMC),三星,Global Foundries(GF),中芯(SMIC)也勉强算一个。还有,今年开始Intel工厂(ICF)也会开放给ARM处理器。事实上有人已经开始做了,只不过用的不是第三方的物理库。通常新工艺会选TSMC,然后要降成本的时候会去UMC。GF一直比较另类,保险起见不敢选,而三星不太理别人所以也没人理他。至于SMIC,嘿嘿,那需要有很高的理想才能选。
16nm的含义我就不具体说了,网上很多解释。而TSMC的16nm又分为很多小节点,FFLL+
,FFC等。他们之间的最高频率,漏电,成本等会有一些区别,适合不同的芯片,比如手机芯片喜欢漏电低,成本低的,服务器喜欢频率高的,不一而足。
接下来看表格第一排,Configuration。这个最容易理解,使用了四核A53,一级数据缓存32KB,二级1MB,打开了ECC和加解密引擎。这几个选项会对面积产生较大影响,对频率和功耗也有较小影响。
接下来是Performance target,目标频率。后端工程师把频率称作Performance,在做后端实现时,必须在频率,功耗,面积(PPA)里选定一个主参数来作为主要优化目标。这个表格是专门为高性能A53做的,频率越高,面积和漏电就会越大,这是没法避免的。稍后我再贴个低功耗小面积的报告做对比。
下面是Current Performance,也就是现在实现了的频率。里面的TT/0.9V/85C是什么意思?我们知道,在一个晶圆(Wafer)上,不可能每点的电子漂移速度都是一样的,而电压,温度不同,它们的特性也会不同,我们把它们分类,就有了PVT(Process,Voltage,
Temperature),分别对应于TT/0.9V/85C。而Process又有很多Conner,类似正态分布,TT只是其中之一,按照电子漂移速度还可以有SS,S,TT,F,FF等等。通常后端结果需要一个Signoff条件(我们这通常是SSG),按照这个条件出去流片,作为筛选门槛,之下的芯片就会不合格,跑不到所需的频率。所以条件设的越低,良率(Yield)就会越高。但是条件也不能设的太低,不然后端很难做,或者干脆方程无解,跑不出结果。X86上有个词叫体质,就是这个PVT。
这一栏有四个频率,上下两组容易区分,就是不同的电压。在频率确定时,动态功耗是电压的2次方,这个大家都知道。而左右两组数字的区别就是Corner了,分别为TT和SSG。
下一行是Optimization PVT。大家都知道后端EDA工具其实就是解方程,需要给他一个优化目标,它会自动找出最优局部解。而1.0V和0.9V中必须选一个值,作为最常用的频率,功耗和面积的甜点(Sweet Spot)。这里是选了1.0V,它的SSG和目标要求更接近,那些达不到的Corner可以作为降频贱卖。
再下一行是漏电Leakage,就是静态功耗。CPU停在那啥都不跑也会有这个功耗,它包含了四个CPU中的逻辑和一级缓存的漏电。但是A53本身是不包含二级缓存的,其他的一些小逻辑,比如SCU(Snooping Control Unit)也在CPU核之外,这些被称作Non-CPU,包含在MP4中。我们待机的时候就是看的它,可以通过power gating关掉二三级缓存,但是通常来说,不会全关,或者没法关。
下面是Dynamic Power,动态功耗。基本上我见过的CPU在测量动态功耗的时候,都是跑的Dhrystone。Dhrystone是个非常古老的跑分程序,基本上就是在做字符串拷贝,非常容易被软件,编译器和硬件优化,作为性能指标基本上只有MCU在看了。但是它有个好处,就是程序很小,数据量也少,可以只运行在一级缓存(如果有的话),这样二级缓存和它之后的电路全都只有漏电。虽然访问二级三级缓存甚至DDR会比访问一级缓存耗费更多的能量,但是它们的延迟也大,此时CPU流水线很可能陷入停顿。这样的后果就是Dhrystone能最大程度的消耗CPU核心逻辑的功耗,比访问二级以上缓存的程序都要高。所以通常都拿Dhrystone来作为CPU最大功耗指标。实际上,是可以写出比Dhrystone更耗电的程序的,称作Max Power Vector,做SoC功耗估算的时候会用上。
动态功耗和电压强相关。公式里面本身就是2次方,然后频率变化也和电压相关,在跨电压的时候就是三次方的关系了。所以别看1.0V只比0.72V高了39%,最终动态功耗可能是3倍。而频率高的时候,动态功耗占了绝大部分,所以电压不可小觑。
此外,动态功耗和温度相关,SoC运行的时候不可能温度维持在0度,所以功耗通常会拿85度或者更高来计算,这个就不多说了。
下一行是Area,面积。面积是芯片公司的立足之本,和毛利率直接相关。所以在性能符合的情况下,越小越好,甚至可以牺牲功耗,不惜推高电压,所以有了OD(Over Drive)。有个数据,当前28nm上,每个平方毫米差不多是10美分的成本,一个超低端的手机芯片怎么也得30mm(200块钱那种手机用的,可能你都没见过,还是智能机),芯片面积成本就是3刀,这还不算封测,储存和运输。低端的也得是40mm(300块的手机)。我们常见的600-700块钱的手机,其中六分之一成本是手机芯片。当然,反过来,也有人不缺钱的,比如苹果,据说A10在16nm上做到了125mm,换算成这里的A53MP4,单看面积不考虑功耗,足足可以放120个A53,极其奢侈,这可是跑在2.8G的A53,如果是1.5G的,150个都可能做到。
那苹果这么大的面积到底是做什么了?首先,像GPU,Video,Display,基带,ISP这些模块,都是可以轻易的拿面积换性能的,因为可以并行处理。而且,功耗也可以拿面积换,一个最简单的方法就是降频,增加处理单元数。这样漏电虽然增加,但是电压下降,动态功耗可以减少很多。一个例外就是CPU的单核性能,为什么苹果可以做到Kirin960的1.8倍,散热还能接受?和物理库,后端,前端,软件都有关系。
首先,A10是6发射,同时代的A73只用了2发射。当然,由于受到了数据和指令相关性限制,性能不是三倍提升,而6发射的后果是面积和功耗非线性增加。作为一个比较,我看过ARM的6发射CPU模型,同工艺下,单核每赫兹性能是A73的1.8倍,动态功耗估算超过2倍,面积也接近2倍。当然,它的微结构和A73是有挺大区别的。这个单核芯片跑在16nm,2.5Ghz,单核功耗差不多是1W。而手机芯片的功耗可以维持在2.5W不降频,所以苹果的2.3Ghz的A10算下来还是可行的。
为了控制功耗,在做RTL的时候就需要插入额外晶体管,做Clock Gating,而且这还是分级的,RTL级,模块级,系统级,信号时钟上也有(我看到的SoC时钟通常占了整个逻辑电路功耗的三分之一)。这样一套搞下来,面积起码大1/3.然后就是Power
Gating,也是分级的。最简单的是每块缓存给一个开关,模块也有一个开关。复杂的根据不同指令,可以计算出哪些Cache
bank短时间内不用,直接给它关了。Power Gating需要的延时会比Clock
Gating大,有的时候如果操作很频繁,Power Gating反而得不偿失,这需要仔细的考量。而且,设计的越复杂,验证也就越难写,这里面需要做一个均衡。除了时钟域,电源域,还有电压域,可以根据不同频率调电压。当然了,域越多,布线越难,面积越大。
再往上,可以定义出不同的power state,让上层软件也参与经来,形成电源管理和调度。
再回到苹果A10,它还使用了6MB的缓存。这个在手机里面也算大的惊世骇俗。通常高端的A73加2MB,A53加1MB,已经很高大上了,低端的加起来也不超过1MB。我拿SPECINT2K在A53做过一些实验,二级缓存从128KB增加到1MB只会增加15%不到的性能,到6MB那性能/面积收益更不是线性的,这是赤裸裸的面积换性能。而且苹果宣扬的不是SPECINT,而是GeekBench4.0,我怀疑是不是这个跑分对缓存大小更敏感,有空可以做做实验。顺带提一句,安兔兔5.0和缓存大小没半毛钱关系,这让广大高端手机芯片公司情何以堪。到了6.0似乎改了,我还没仔细研究过。至于使用了大面积缓存引起的漏电,倒是有办法解决,那就是部分关闭缓存,用多少开多少,是个精细活,需要软硬件同时配合。
影响面积的因素还没完,上面只是前端,后端还有一堆考量呢。
首先就是表格下一排,Metal Stack。芯片制造的时候是一层层蚀刻的,而蚀刻的时候需要一层层打码,免得关键部分见光,简称Mask。这里的11m就表示有11层。晶体管本身是在最底层的,而走线就得从上面走,层数越多越容易,做板子布线的同学肯定一看就明白了。照理说这就该多放几层,但是工厂跟你算钱也是按照层数来的,越多越贵。层数少了不光走线难,总体面积的利用率也低,像A53,11层做到80%的利用率就挺好了。所以芯片上不是把每个小模块面积求和就是总体面积,还得考虑布局布线(PR,Placing&Routing),考虑面积利用率。
再看表格下两排,Logic Architecture和Memory。这个也容易理解,就是逻辑和内存,数字电路的两大模块分类。这个内存是片上静态内存,不是外面的DDR。uLVT是什么意思呢,Ultra Low Voltage
Threshold,指的是标准逻辑单元(Standard Cell)用了超低电压门限。电压低对于动态功耗当然是个好事,但是这个标准单元的漏电也很高,和频率是对数关系,也就是说,漏电每增加10倍,最高频率才增加log10%。后端可以给EDA工具设一个限制条件,比如只有不超过1%的需要冲频率的关键路径逻辑电路使用uLVT,其余都使用LVT,SVT或者HVT(电压依次升高,漏电减小),来减小总体漏电。
对于动态功耗,后端还可以定制晶体管的源极和漏极的长度,越窄的电流越大,漏电越高,相应的,最高频率就可以冲的更高。所以我们有时候还能看到uLVT C16,LVT C24之类的参数,这里的C就是指Channel Length。
接下去就是Memory,又作Memory Instance,也有人把它称作FCI(Fast Cache Instance)。访问Memory有三个重要参数,read,write和setup。这三个参数可以是同样的时间,也可以不一样。对于一级缓存来说基本用的是同样的时间,并且是一个时钟周期,而且这当中没法流水化。从A73开始,我看到后端的关键路径都是卡在访问一级缓存上。也就是说,这段路径能做多快,CPU就能跑到多快的频率,而一级缓存的大小也决定了索引的大小,越大就越慢,频率越低,所以ARM的高端CPU一级缓存都没超过64KB,这和后端紧密相关。当然,一级缓存增大带来的收益本身也会非线性减小。之后的二三级缓存,可以使用多周期访问,也可以使用多bank交替访问,大小也因此可以放到几百KB/几MB。
逻辑和内存统称为Physical Library,物理库,它是根据工厂给的每个工艺节点的物理开发包(PDK)设计的,而Library是一个Fabless芯片公司能做到的最底层。能够定制自己的成熟物理库,是这家公司后端领先的标志之一。
最后一行,Margin。这是指的工厂在生产过程中,肯定会产生偏差,而这行指标定义了偏差的范围。如下图:
蓝色表示我们刚才说的一些Corner的分布,红色表示生产偏差Variation。必须做一些测试芯片来矫正这些偏差。SB-OCV表示stage-based on-chip variation,和其他的几个偏差加在一起,总共+-7%,也就是说会有7%的芯片不在后端设计结束时确定的结果之内。
后面还有一些setup UC之类的,表示信号建立时间,保持时间的不确定性(Uncertainty),以及PLL的抖动范围。
至此,一张报告解读完毕,我们再看看对应的低功耗版实现版本:
这里频率降到1.5G左右,每Ghz动态功耗少了10%,但是静态降到了12.88mW,只有25%。我们可以看到,这里使用了LVT,没有uLVT,这就是静态能够做低的原因之一。由于面积不是优化目标,它基本没变,这个也是可以理解的,因为Channel宽度没变,逻辑的面积没法变小,逻辑部分的低利用率也使得变化基本看不出来。
ARM攒机指南-5G乱弹
前一阵有个同事突发奇想,说5G基带芯片可不可以用GPU来做通用计算,反正都是乘加嘛。这样既省了基带的面积(有个参考数据,28nm时候,4G CAT7要十几个平方毫米,而低端的手机芯片一共也就30-40mm的预算,中端的也不过60mm),而数据传输率不高的时候,多出来的GPU核还可以拿来打游戏,多好。想法不错,于是我们就开始算PPA,看看靠不靠谱。
首先,上一个传统4G基带模块图:
输入大致需要滤波,解码,FFT,均衡,解交织,信道估算等步骤,输出就简单多了,省了滤波和解码,信道估算等。上图使用了DSP来做数据通路外加一层控制,二三四层协议放在CPU做,用DMA来搬数据。这个结构,做LTE CAT4问题不大,毕竟CAT4下载才150Mbps。但是到了5G就不一样,5G的传输率在一下子飞跃到10Gbps,而第一代也得2-3Gbps左右,这运算量一下子高了20-60倍。
于是做了下估算。假设5G是8x8MIMO,8路通道,那么滤波需要运算量如下:
30.72Mbps(每通道数据传输率) x 8(通道数) x 4(过采样) = 1Gbps。
这意味着1Gbpsx32(32阶矩阵)x2(正反变换)=64G次的16x16MAC(乘加)运算。这是每秒钟要完成的计算量。对应到DSP上,一个每周期能做4次MAC的DSP核,运行在1Ghz,需要16个。这得是多大的面积和功耗啊,而这只是第一个环节。第二大计算模块是解码,按照经验大概推了下,需要5G MAC/s。其余的就小了,iFFT大概224M MAC/s,负载均衡,解交织等加起来也不过100M MAC,可以忽略不计。
总的来说,10Gbps的线速需要70G MAC/s,用128MAC/cycle的DSP需要跑在600MHz,16纳米上面积估计3mm左右。那用GPU需要多少个核呢?拿ARM公布的G71来算,假设带宽和延迟不是问题,跑在850Mhz,每个核的峰值运算能力是FP32 30GFLOPS,相当于60G次16x16 MAC,几乎一个核就能搞定。而在16nm上,G71跑在850Mhz的动态功耗是250mW/Ghz,漏电是60mW,总体不到300mW,而面积是3mm,看上去似乎可行。
不过,用GPU做滤波和解码有个非常大的问题,就是延迟。GPU的驱动是跑在CPU上的,GPU指令动态生成,所有的数据buffer全都要分配好,然后丢给GPU。这个过程是毫秒级的。用做图形无所谓,因为60帧刷新的话,足足有16ms的时间完成顶点和渲染,而一般的通用计算只要求平均性能,不会要求每一帧的延迟。到了基带就不一样,有些命令和信号解析的处理必须是在几十个微秒内完成,也许计算量并不大但是延迟有要求。而GPU的现有驱动结构很难做到这点。
还有一点,对于频域信号FDD,某些下载场景,哪怕下载速率相对不大,GPU都必须一直开着并全力计算,不能开一小段时间,做完处理就关闭。这样,300mW的功耗其实就非常大了。在场测时,这样的场景是必须测试的,此时功耗就比asic方案大。
所以,用GPU做基带通用计算就不太可行。而用DSP可以解决延迟的问题,对于70G
MAC/s来说,功耗和面积应该是Asic的2倍以上,还是有点大。所以我觉得,靠谱的方案还是在数据通路上使用ASIC做大部分的计算。功耗可以降到几十毫瓦,面积更是可以缩小。
我这有张5G基带芯片的结构图:
和4G不同,这个结构里大量使用ASIC模块,还出现了一个VPU。这个概念其实来自于高通的设计,高通自己定制了一个向量处理器,把基带常用的计算做成指令放进去,具体情况不清楚,据说很好用。
数据通路解决了,接下来还有控制通路,也就是图上的棕色部分,使用了Cortex-R8。相比4G,5G的控制通路性能需求也是涨得非常快。有一个数据,拿R8单核跑4G LTE 2-4层协议栈,600Mbps就需要跑在600Mhz左右。以此类推,做5Ggen1的控制,数据率在3Gbps,就需要3-4个跑在1Ghz的R8,好在R8最大可以支持到MP4,此时的面积倒是不大,MP4在28nm上3mm,功耗300多mW。这个300mW不像数据通路前端部分不能关,经过频域时域转换,如果没有数据进来,是可以降低运行频率,甚至关掉几个核留一个待机的。MP4的好处是核之间可以有双向硬件一致性,对于某个数据包做处理,分别要经过1,2,3,4核做不同工序的话,就可以完全不用软件刷新缓存了,这其实省了非常多的时间。因为一个32KB的缓存刷新时间肯定是毫秒级的,而如果每个数据包只有1KB大小,那相当于每传1KB就要刷新一次一级数据缓存,因为程序并不知道一级数据缓存的那些部分被用了,只能统统刷走。当然,还有一些别的方法可以做优化,比如数据分类,只需要用一次的就把页属性设成non-cacheable,不要占用缓存,而经常要用的可以多个包一起刷,降低overhead。这些都和系统设计有关,不管AMP/SMP都用得上。之前的4G基本都是用单核来做所有的协议处理,到了5G就必须考虑多核情况了。
不过我在想,这才是3Gbps而已,10Gbps的全速5G怎么办?难道基带也得用10核吗?是不是得像网络处理一样,加个ASIC自动合并类似的包,或者干脆都使用大包?
ARM攒机指南-AI篇
近一年各种深度学习平台和硬件层出不穷,各种xPU的功耗和面积数据也是满天飞,感觉有点乱。在这里我把我看到的一点情况做一些小结,顺便列一下可能的市场。在展开之前,我想强调的是,深度学习的应用无数,我能看到的只有能在千万级以上的设备中部署的市场,各个小众市场并不在列。
深度学习目前最能落地的应用有两个方向,一个是图像识别,一个是语音识别。这两个应用可以在如下市场看到:个人终端(手机,平板),监控,家庭,汽车,机器人,服务器。
先说手机和平板。这个市场一年的出货量在30亿颗左右(含功能机),除苹果外总值300亿刀。手机主要玩家是苹果(3亿颗以下),高通(8亿颗以上),联发科(7亿颗以上),三星(一亿颗以下),海思(一亿颗),展讯(6亿颗以上),平板总共4亿颗左右。而28纳米工艺,量很大的话(1亿颗以上),工程费用可以摊的很低,平均1平方毫米的成本是8美分左右,低端4G芯片(4核)的面积差不多是50平方毫米以下,成本就是4刀。中端芯片(8核)一般在100平方毫米左右,成本8刀。16纳米以及往上,同样的晶体管数,单位成本会到1.5倍。一般来说,手机的物料成本中,处理器芯片(含基带)价格占了1/6左右。一个物料成本90刀的手机,用的处理器一般在15刀以下,甚至只有10刀。这个10刀的芯片,包含了处理器,图形处理器,基带,图像信号处理器,每一样都是高科技的结晶,却和肯德基全家桶一个价,真是有点惨淡。然而生产成本只是一部分,人力也是很大的开销。一颗智能机芯片,软硬开发,测试,生产,就算全用的成熟IP,也不会少于300人,每人算10万刀的开销,量产周期两年,需要6000万刀。外加各种EDA工具,IP授权和开片费,芯片还没影子,1亿刀就下去了。
言归正传,手机上的应用,最直接的就是美颜相机,AR和语音助手。这些需求翻译成硬件指令就是对8位整数点乘(INT8)和16位浮点运算(FP16)的支持。具体怎么支持?曾经看到过一张图,我觉得较好的诠释了这一点:
智能手机和平板上,是安卓的天下,所有独立芯片商都必须跟着谷歌爸爸走。谷歌已经定义了Android NN作为上层接口,可以支持它的TensorFlow以及专为移动设备定义的TensorFlow Lite。而下层,针对各种不同场景,可以是CPU,GPU,DSP,也可以是硬件加速器。它们的能效比如下图:
可以看到,在TSMC16纳米工艺下,大核能效比是10-100Gops/W(INT8),小核可以做到100G-1Tops/W,手机GPU是300Gops/W,而要做到1Tops/W以上,必须使用加速器。这里要指出的是,小核前端设计思想与大核完全不同,在后端实现上也使用不同的物理单元,所以看上去和大核的频率只差50%,但是在逻辑运算能效比上会差4倍以上,在向量计算中差的就更多了。
手机的长时间运行场景下,芯片整体功耗必须小于2.5瓦,分给深度学习任务的,不会超过1.5瓦。相对应的,如果做到1Tops/W,那这就是1.5T(INT8)的处理能力。对于照片识别而言,情况要好些,虽然对因为通常不需要长时间连续的处理。这时候,CPU是可以爆发然后休息的。语音识别对性能要求比较低,100Gops可以应付一般应用,用小核也足够。但有些连续的场景,比如AR环境识别,每秒会有30-60帧的图像送进来,如果不利用前后文帮助判断,CPU是没法处理的。此时,就需要GPU或者加速器上场。
上图是NVidia的神经网络加速器DLA,它只有Inference的功能。前面提到在手机上的应用,也只需要Inference来做识别,训练可以在服务端预先处理,训练好的数据下载到手机就行,识别的时候无需连接到服务端。
DLA绿色的模块形成类似于固定的流水线,上面有一个控制模块,可以用于动态分配计算单元,以适应不同的网络。稀疏矩阵压缩减少带宽,优化的矩阵算法减少计算量,外加SRAM(一个273x128, 128x128, 128x128 ,128x6 的4层INT8网络,需要70KBSRAM)。我看到的大多数加速器其实都是和它大同小异,有些加速器增加了一个SmartDMA引擎,可以通过简单计算预取所需的数据。根据我看到的一些跑分测试,这个预取模块可以把计算单元的利用率提高到90%以上。
至于能效比,我看过的加速器,在支持INT8的算法下,可以做到1.2Tops/W (1Ghz@T16FFC),1Tops/mm^2,并且正在向1.5Tops/W靠近。也就是说,1.5W可以获得2Tops(INT8)的理论计算能力。这个计算能力有多强呢?我这目前处理1080p60FPS的图像中的60x60及以上的像素大小的人脸识别,大致需要0.5Tops的计算能力,2Tops完全可以满足。当然,如果要识别复杂场景,那肯定是计算力越高越好。
为什么固定流水的能效比能做的高?ASIC的能效比远高于通用处理器已经是一个常识,更具体一些,DLA不需要指令解码,不需要指令预测,不需要乱序执行,流水线不容易因为等待数据而停顿。下图是某小核各个模块的动态功耗分布,计算单元只占1/3,而指令和缓存访问占了一半。
有了计算量,深度学习加速器对于带宽的需求是多少?如果SRAM足够大,1Tops的计算量需要5GB/s以下的带宽。连接方法可以放到CPU的加速口ACP(跑在1.8GHz的ARMv8.2内部总线可以提供9GB/s带宽)。只用一次的数据可以设成非共享类型,需要和CPU交换或者常用的数据使用Cacheable和Shareable类型,既可以在三级缓存分配空间,还可以更高效的做监听操作,免掉刷缓存。
不过,上述前提成立的前提是权值可以全部放到SRAM或者缓存。对于1TOPS INT8的计算量,所需权值的大小是512GB/s(有重复)。如果全部放DDR,由于手机的带宽最多也就是30GB/S,是完全不够看的。对于输入,中间值和输出数据,我在上文有个例子,一个273x128, 128x128, 128x128 ,128x6 的4层INT8网络,需要70KB的SRAM(片内)放权值,共7万个。但是输入,输出和中间结果加起来却只有535个,相对来说并不大。这里的运算量是14万次(乘和加算2次)。对于1T的运算量来说,类似。中间数据放寄存器,输出数据无关延迟,只看带宽,也够。最麻烦的就是权值,数据量大到带宽无法接受。所以只能把权值放进SRAM防止重复读取,从而免掉这500GB/s带宽。我看到的有些深度学习的算法,权值在几十到200兆,这样无论如何是塞不进SRAM的。哪怕只有10%需要读入,那也是50GB/s的带宽。虽说现在有压缩算法压缩稀疏矩阵,有论文达到30-50倍的压缩率,但我看到的实际识别算法,压缩后至少也是20MB,还是塞不进SRAM。
此外,移动端仅仅有神经网络加速器是远远不够的。比如要做到下图效果,那首先要把人体的各个细微部位精确识别,然后用各种图像算法来打磨。而目前主流图像算法和深度学习没有关系,也没看到哪个嵌入式平台上的加速器在软件上有很好的支持。目前图像算法的支持平台还主要是PC和DSP,连嵌入式GPU做的都一般。
那这个问题怎么解决?我看到两种思路:
第一种,GPU内置加速器。下图是Verisilicon的Vivante改的加速器,支持固定流水的加速器和可编程模块Vision core(类似GPU中的着色器单元),模块数目可配,可以同时支持视觉和深度学习算法。不过在这里,传统的图形单元被砍掉了,以节省功耗和面积。只留下调度器等共用单元,来做异构计算的调度。
这类加速器比较适合于低端手机,自带的GPU和CPU本身并不强,可能光支持1080p的UI就已经耗尽GPU资源了,需要额外的硬件模块来完成有一定性能需求的任务。
对于中高端手机,GPU和CPU的资源在不打游戏的时候有冗余,那么就没有必要去掉图形功能,直接在GPU里面加深度学习加速器就可以,让GPU调度器统一调度,进行异构计算。
上图是某款GPU的材质计算单元,你有没有发现,其实它和神经网络加速器的流水线非常类似?都需要权值,都需要输入,都需要FP16和整数计算,还有数据压缩。所不同的是计算单元的密度,还有池化和激活。稍作改动,完全可以兼容,从而进一步节省面积。
但是话说回来,据我了解,目前安卓手机上各种图像,视频和视觉的应用,80%其实都是用CPU在处理。而谷歌的Android NN,默认也是调用CPU汇编。当然,手机芯片自带的ISP及其后处理,由于和芯片绑的很紧,还是能把专用硬件调动起来的。而目前的各类加速器,GPU,DSP,要想和应用真正结合,还有挺长的路要走。
终端设备上还有一个应用,AR。据说iPhone8会实现这个功能,如果是的话,那么估计继2015的VR/AR,2016的DL,2017的NB-IOT之后,2018年又要回锅炒这个了。
那AR到底用到哪些技术?我了解的如下,先是用深度传感器得到场景深度信息,然后结合摄像头拍到的2维场景,针对某些特定目标(比如桌子,面部)构建出一个真实世界的三维物体。这其中需要用到图像识别来帮助判断物体,还需要确定物体边界。有了真实物体的三维坐标,就可以把所需要渲染的虚拟对象,贴在真实物体上。然后再把摄像头拍到的整个场景作为材质,贴到背景图层,最后把所有这些图层输出到GPU或者硬件合成器,合成最终输出。这其中还需要判断光源,把光照计算渲染到虚拟物体上。这里每一步的计算量有多大?
首先是深度信息计算。获取深度信息目前有三个方法,双目摄像头,结构光传感器还有TOF。他们分别是根据光学图像差异,编码后的红外光模板和反射模板差异,以及光脉冲飞行时间来的得到深度信息。第一个的缺点是需要两个摄像头之间有一定距离并且对室内光线亮度有要求,第二个需要大量计算并且室外效果不佳,第三个方案镜头成本较高。据说苹果会用结构光方案,主要场景是室内,避免了缺点。结构光传感器的成本在2-3刀之间,也是可以接受的。而对于计算力的要求,最基本的是对比两个经过伪随机编码处理过的发射模板以及接受模板,计算出长度差,然后用矩阵倒推平移距离,从而得到深度信息。这可以用专用模块来处理,我看到单芯片的解决方案,720p60FPS的处理能力,需要20GFLOPS FP32的计算量以上。换成CPU,就是8核。当然,我们完全可以先识别出目标物体,用图像算法计算出轮廓,还可以降低深度图的精度(通常不需要很精确),从而大大降低计算量。而识别本身的计算量前文已经给出,计算轮廓是经典的图像处理手段,针对特定区域的话计算量非常小,1-2个核就可以搞定。
接下去是根据深度图,计算真实物体的三维坐标,并输出给GPU。这个其实就是GPU渲染的第一阶段的工作,称作顶点计算。在移动设备上,这部分通常只占GPU总计算量的10%,后面的像素计算才是大头。产生虚拟物体的坐标也在这块,同样也很轻松。
接下去是生成背景材质,包括产生minimap等。这个也很快,没什么计算量,把摄像头传过来的原始图像放到内存,告诉GPU就行。
稍微麻烦一些的是计算虚拟物体的光照。背景贴图的光照不需要计算,使用原图中的就可以。而虚拟物体需要从背景贴图抽取亮度和物体方向,还要计算光源方向。我还没有见过好的算法,不过有个取巧,就是生成一个光源,给一定角度从上往下照,如果对AR要求不高也凑合了。
其他的渲染部分,和VR有些类似,什么ATW啊,Front Buffer啊,都可以用上,但是不用也没事,毕竟不是4K120FPS的要求。总之,AR如果做的不那么复杂,对CPU和GPU的性能要求并不高,搞个图像识别模块,再多1-2个核做别的足矣。
如果加速器在GPU上,那么还是得用传统的ACE口,一方面提高带宽,一方面与GPU的核交换数据在内部进行,当然,与CPU的交互必然会慢一些。
在使用安卓的终端设备上,深度学习可以用CPU/DSP/GPU,也可以是加速器,但不管用哪个,一定要跟紧谷歌爸爸。谷歌以后会使用Vulkan
Compute来替代OpenCL,使用Vulkan 来替代OpenGL ES,做安卓GPU开发的同学可以早点开始学习。
第二个市场是家庭,包括机顶盒/家庭网关(4亿颗以下),数字电视(3亿颗以下),电视盒子(1亿以下)三大块。整个市场出货量在7亿片,电器里面的MCU并没有计算在内。这个市场公司比较散,MStar/海思/博通/Marvell/Amlogic都在里面,小公司更是无数。如果没有特殊要求,拿平板的芯片配个wifi就可以用。当然,中高端的对画质还是有要求,MTK现在的利润从手机移到了电视芯片,屏幕显示这块有独到的技术。很多机顶盒的网络连接也不是以太网,而是同轴电缆等,这种场合也得专门的芯片。
最近,这个市场里又多了一个智能音箱,各大互联网公司又拿出当年追求手机入口的热情来布局,好不热闹。主要玩家如下:
其中,亚马逊和谷歌占大头,芯片均采用ARM Cortex-A小核做控制器,DSP做图像和语音处理的方式。其中,DSP的运算能力在10Gops的INT8 MAC左右,并不高,价格却不便宜,大于20美金。在芯片内部,DSP的主要作用还是回声消除,去噪,语音识别等。自然语言理解和神经网络计算并不是在设备端,而是在云端。在国内,百度和科大讯飞提供SDK甚至模块,不过还是需要连到云端才能启用完整功能。在芯片方面,国内有些公司已经发布了一些带深度学习加速器的芯片,并集成语音处理模块和内存颗粒。未来这类芯片会更多,而软件平台,或者说语义处理到地方在云端还是终端,会成为争夺的焦点。
对于语音设别,如果是需要做自然语言理解,性能可能要到100Gops。对于无风扇设计引入的3瓦功耗限制,CPU/DSP和加速器都可以选。不过工艺就得用28纳米了或者更早的了,毕竟没那么多量,撑不起16纳米。最便宜的方案,可以使用RISC-V+DLA,没有生态系统绑定的情况下最省成本。
家庭电子设备里还有一个成员,游戏机。Xbox和PS每年出货量均在千万级别。VR/AR和人体识别早已经用在其中。
接下去是监控市场。监控市场上的图像识别是迄今为止深度学习最硬的需求。监控芯片市场本身并不大,有1亿颗以上的量,销售额20亿刀左右。主流公司有安霸,德州仪器和海思,外加几个小公司,OEM自己做芯片的也有。
传统的监控芯片数据流如上图蓝色部分,从传感器进来,经过图像信号处理单元,然后送给视频编码器编码,最后从网络输出。如果要对图像内容进行识别,那可以从传感器直接拿原始数据,或者从ISP拿处理过的图像,然后进行识别。中高端的监控芯片中还会有个DSP,做一些后处理和识别的工作。现在深度学习加速器进来,其实和DSP是有些冲突的。以前的一些经典应用,比如车牌识别等,DSP其实就已经做得很好了。如果要做识别以外的一些图像算法,这颗DSP还是得在通路上,并不能被替代。并且,DSP对传统算法的软件库支持要好得多。这样,DSP替换不掉,额外增加处理单元在成本上就是一个问题。
对于某些低功耗的场景,我看到有人在走另外一条路。那就是完全扔掉DSP,放弃存储和传输视频及图像,加入加速器,只把特征信息和数据通过NB-IOT上传。这样整个芯片功耗可以控制在500毫瓦之下。整个系统结合传感器,只在探测到有物体经过的时候打开,平时都处于几毫瓦的待机状态。在供电上,采用太阳能电池,100mmx100mm的面板,输出功率可以有几瓦。不过这个产品目前应用领域还很小众。
做识别的另一个途径是在局端。如果用显卡做,GFX1080的FP32 GLOPS是9T,180瓦,1.7Ghz,16纳米,320mm。而一个Mali
G72MP32提供1T FP32的GFLOPS,16纳米,850Mhz,8瓦,9T的话就是72瓦,666mm。当然,如果G72设计成跑在1.7Ghz,我相信不会比180瓦低。此外桌面GPU由于是Immediate rendering的,带宽大,但对缓存没有很大需求,所以移动端的GPU面积反而大很多,但相对的,它对于带宽需求小很多,相应的功耗少很多。
GPU是拿来做训练的,而视频识别只需要做Inference,如果用固定流水的加速器,按照NVIDIA Tesla P40的数据,48T INT8 TOPS,使用固定流水加速器,在16nm上只需要48mm。48Tops对应的识别能力是96路1080p60fps,96路1080p60fps视频解码器对应的面积差不多是
50mm,加上SRAM啥的,估计200mm以下。如果有一千万的量,那芯片成本可以做到40美金以下(假定良率还可以,不然路数得设计的小一点),而一块Tesla P40板子的售价是500美金(包括DDR颗粒),还算暴利。国内现在不少小公司拿到了投资在做这块的芯片。
第四个市场是机器人/无人机。机器人本身有多少量我没有数据,手机和平板的芯片也能用在这个领域。无人机的话全球一年在200万左右,做视觉处理的芯片也应该是这个量级。。用到的识别模块目前看还是DSP和CPU为主,因为DSP还可以做很多图像算法,和监控类似。这个市场对于ISP和深度信息的需求较高,双摄和结构光都可以用来算深度计算,上文提过就不再展开。
在无人机上做ISP和视觉处理,除了要更高的清晰度和实时性外,还比消费电子多了一个要求,容错。无人机的定位都靠视觉,如果给出的数据错误或者模块无反应都不符合预期。解决这个问题很简单,一是增加各种片内存储的ECC和内建自检,二是设两个同样功能的模块,错开时钟输入以避免时钟信号引起的问题,然后输出再等相同周期,同步到一个时钟。如果两个结果不一致,那就做特殊处理,避免扩散数据错误。
第五个市场是汽车,整个汽车芯片市场近300亿刀,玩家众多:
在汽车电子上,深度学习的应用就是ADAS了。在ADAS里面,语音和视觉从技术角度和前几个市场差别不大,只是容错这个需求进一步系统化,形成Function Safety,整个软硬件系统都需要过认证,才容易卖到前装市场。Function Safety比之前的ECC/BIST/Lock Step更进一步,需要对整个芯片和系统软件提供详细的测试代码和文档,分析在各类场景下的错误处理机制,连编译器都需要过认证。认证本身分为ASIL到A-ASIL-D四个等级,最高等级要求系统错误率小于1%。我对于这个认证并不清楚,不过国内很多手机和平板芯片用于后装市场的ADAS,提供语音报警,出货量也是过百万的。
最后放一张ARM的ADAS参考设计框图。
可能不会有人照着这个去设计ADAS芯片,不过有几处可以借鉴:
右方是安全岛,内涵Lock Step的双Cortex-R52,这是为了能够保证在左边所有模块失效的情况下复位整个系统或者进行异常中断处理的。中部蓝色和绿色的CryptoCell模块是对整个系统运行的数据进行保护,防止恶意窃取的。关于Trustzone设计我以前的文章有完整介绍这里就不展开了。
以上几个市场基本都是Inference的需求,其中大部分是对原有产品的升级,只有ADAS,智能音箱和服务器端的视频识别检测是新的市场。其中智能音箱达到了千万级别,其它的两个还都在扩张。
接下去的服务端的训练硬件,可以用于训练的移动端GPU每个计算核心面积是1.5mm(TSMC16nm),跑在1Ghz的时候能效比是300Gops/W。其他系统级的性能数据我就没有了。虽然这个市场很热,NVidia的股票也因此很贵,但是我了解到全球用于深度学习训练的GPU销售额,一年只有1亿刀不到。想要分一杯羹,可能前景并没有想象的那么好。
最近970发布,果然上了寒武纪。不过2T ops FP16的性能倒是让我吃了一惊,我倒推了下这在16nm上可能是6mm的面积,A73MP4+A53MP4(不含二级缓存)也就是这点大小。麒麟芯片其实非常强调面积成本,而在高端特性上这么舍得花面积,可见海思要在高端机上走出自己的特色之路的决心,值得称道。不过寒武纪既然是个跑指令的通用处理器,那除了深度学习的计算,很多其他场合也能用上,比如ISP后处理,计算结构光深度信息等等,能效可能比DSP还高些。
ARM攒机指南-媒体篇
把手机芯片的架子搭好后,需要看看怎么加入多媒体部分。
所谓多媒体,包含三个模块:图形处理器(GPU),显示模块(Display),视频模块(Video)。显示模块负责把所有的内容输出到屏幕,视频模块负责解码片源,也负责编码摄像头的录制内容。图像信号处理(ISP)模块暂时不算在内,以后另说。
GPU是大家喜闻乐见,津津乐道的部分,各种跑分评测都会把GPU性能重点考量。但是实际上,在定义一个手机芯片多媒体规格的时候,我们首先要确定的参数,不是GPU有多强大,而是显示输出的分辨率:是720p,1080p,2K还是更高。这个参数,决定了GPU填充率(fill rate)的下限,系统带宽大小,内存控制器的数量,还会影响CPU和ISP的选择,从而决定整体功耗及成本。所以,这一参数至关重要。
举几个典型的例子:
超低端:展讯的SC9832,显示分辨率720p,ARM Cortex-A7MP4,Mali400MP2, 1xLPDDR3
低端:联发科的MT6739,显示分辨率1444x720,ARM Cortex-A53MP4@1.5GHz,IMG PowerVR GE8100,1xLPDDR3
中端:高通的骁龙652,显示分辨率2560x1600,Cortex A72MP4/Cortex A53MP4,Adreno 510,2xLPDDR3
高端:海思的麒麟970,显示分辨率不高于2K,Cortex A73MP4/Cortex A53MP4,G72MP12,4xLPDDR4
我们可以看到,随着显示分辨率上升,芯片规格越来越高,但到2K就停止了。下面通过定量分析,让我们看看其中每个参数背后的考量。
如上图所示,手机多媒体一定包含图形,视频和显示三个模块。为什么桌面图形处理器囊括了视频和显示输出,而手机要把它们分开?再极端一点,其实所有的多媒体和图像处理都是计算,为什么不全用CPU做了?把省下来的面积全做成CPU,岂不更好?不好意思,这不可行。原因很简单,功耗。请记住,由于没有风扇,手机芯片无论怎么设计,在各种长时间运行场景下,功耗一定得低于2.5瓦,短时间运行也不宜超过5瓦,瞬时运行倒是可以更高。这个2.5瓦,除了跑多媒体,还得包括CPU,总线和内存带宽。而多媒体的每个模块,在做其擅长的事情时,功耗远低于CPU。16nm上视频编解码器在处理4K30FPS帧的功耗在60毫瓦左右,而相同的事情让CPU做,我粗略的估计了下,至少得四个跑在2Ghz的A53,还得是NEON指令(功耗为Dhrystonex2.5),那就是1.5瓦以上的功耗。相差25倍。
再看显示模块的功耗,分辨率2K60帧下,16nm工艺需要50毫瓦左右。而用GPU做相同的事情,粗略的算,需要300毫瓦左右。用CPU还要乘以2到3,近1瓦。
所以,只是放个4K的视频并输出到屏幕,就已经到了功耗上限了,还没有计算访存功耗呢,更不用说支持10小时以上的视频播放。所以,手机多媒体必须把GPU,视频和显示模块分化出来。
当然,如果手机非常低端,一定要用CPU来进行软件解码,从而省了硬件面积(1080p30fps解码是1平方毫米,不到A53单核2倍),也不是完全不可行。因为超低端可能只要支持1080p视频就可以了,并且由于CPU数量小还是小核,功耗虽然高些但也不是完全不能接受。同时,低端的手机CPU本身处理能力弱,软件优化和多核负载均衡一定要做好。而中高端手机不会为了省面积这么做的。
看到这可能有人会问,为什么视频是4K的,而显示只有2K呢?分辨率不匹配,多出来的像素不是浪费么?确实如此。不过由于受到手机屏的限制,目前就算高端手机也还没支持4K。并且屏幕分辨率提高一倍,功耗也提高一倍。这对于本就是耗电大户的屏幕来说,是个大问题,要解决就等着以后更低功耗的屏幕出现了。
反过来,为什么不把视频解码降到2K呢?那是因为视频源的格式是片源决定的,4K的片源没法用2K的解码器去解,只能解完再降分辨率。
还有一个问题,为什么上文中视频是30帧,而显示是60帧呢?我曾经做过实验,特意把屏幕刷新率改成30,结果完全没发现什么不同。但是,据说大部分人的眼神比较好,动态视觉强,对于非自然图像很敏感,所以对于手机背景等图像,一定要做到60帧才能感到流畅。而对于自然图像,比如看视频,30帧就感到流畅了。所以,这两类刷新率就约定成俗了。
下面,我们来看下,屏幕分辨率是如何影响GPU,系统带宽以及内存控制器的。先看下图的显示模块。
显示模块的任务和操作系统的用户界面(UI)中图层的概念有关系。我们看到的最终屏幕画面就是多层图层合成叠加的。同时,显示模块还可以对每一层进行旋转,缩放等操作,最终生成一幅图,转成所需的信号格式输出。显示模块的输入可以是解码后的视频,也可以是GPU丢过来的完成初步合成的图层。上图中,显示模块支持3路输入,外加背景图输入(Smart Layer),我们一般关注前者。
以安卓为例,用到的图层一般在4-8层。假设显示是1080p60fps,那每一层的带宽就是1920x1080x60x4(RGBA)=480MB/s,8层就是4GB/s。这是系统给显示模块的输入,总线上还得有输出。假设这时候再播放4K30fps视频,所需带宽未压缩是1.2Gx1.25=1.5GB/s,
如下:
而GPU跑用户界面时的典型带宽开销如下:
根据UI的复杂度不同,每60帧需要的带宽可达到1GB/s(压缩后),没压缩时在1.5-2GB/s。其他的还有CPU跑驱动,APP等开销,加一起算1GB/s的话,总共9GB/s左右。
单通道的LPDDR4带宽大致在12.8GB/s,如果带宽利用率70%,那差不多正好用满一个DDR控制器,此时每GB带宽消耗在DDR PHY的功耗是100毫瓦(16nm),加上总线和DDR控制器的功耗,总共需要1瓦左右。
这里我们可以算出来,除了CPU/GPU/Video/Display之外,带宽也非常费电,而且增加带宽会较明显的增加内存控制器,DDR PHY和内存颗粒数量,成本上升。相应增加的总线面积和功耗到相对并不大,可以忽略。至于解决由此带来的复杂度,那是设计SoC的基本功,架构篇提过,这里不再重复。
至此,我们已经可以看到如何由显示分辨率反推对于系统带宽,功耗和成本的需求。而GPU的最小需求也可以由此推导出来。
在上图我们可以看到GPU有三个参数,三角形输出率,像素填充率和理论浮点性能。对用户界面来说,意义最大的是像素填充率。
填充率有什么意义?对于上文提到过的每层图层,如果分辨率是1080p,那就需要1920x1080x60=120M/s的像素填充率。如果8个图层全部由GPU画出,那么就需要1G/s的填充率,对应上图的MP2。这还不止。还记得显示模块里面的合成,缩放和旋转功能吗?这些其实GPU也能做。如果显示模块能力不够,只支持4路输入,那我们就需要GPU把8层图层先合并为4层,然后才能交给显示模块。每两层合成相当于重画一层,于是又额外的需要4层,共1440M/s的像素。如果还涉及缩放和旋转,那还需要更多。通常来说,显示模块不会支持到8层,因为这样的场景并不多,会造成硬件冗余。而极端场景下,GPU就被用来完成额外工作,增加灵活性,又能防止屏幕因图层过多造成的卡顿。
当然,由于系统延迟和带宽的存在,像素利用率不可能达到100%.之前的几年,我看到的有些系统只能做到70%的利用率,主要原因是平均延迟太长,而并行度不够大.这时候,简单的增加GPU核心数量并不是一个明智选择,并且如果瓶颈是在系统带宽不够,或者系统调度没做好,即使增加像素输出率也无济于事。近两年的手机芯片基本上可以做到90%的利用率.但是,就算是低端手机,还是会留出更多的填充能力,来应付多图层下复杂操作的突发情况.此时,提高利用率的意义就成了减小功耗.
此外,在很多移动GPU上,像素填充率还意味着同等的材质填充率。因为用户界面基本都是拿图片或者材质来贴图然后混合,不需要大量计算三维图形,三角形输出和浮点能力用处不大,但是材质填充率必须匹配。
按照上文的功耗和面积,下面我们来看两个极端的例子:
支持VR的芯片,显示分辨率4K120fps(双眼),在虚拟房间内播放4k视频,显示模块支持8路输入,那么就可能需要4x1080x1920x120x7=6.4G/s的像素填充率,外加一路4k视频解码。换成GPU就是至少G72MP8,而考虑3D性能,MP16都是不够的。仅仅GPU部分的面积就要36平方毫米,功耗6瓦,系统不加风扇没法跑。
低端的芯片,仅支持1080p,4k30帧播放视频,4层场景,对于G72MP1就能搞定,面积2平方毫米,功耗0.4瓦。加上视频和显示模块也不会超过5个平方毫米,功耗之前我们也算过,较低。
这里还没有考虑GPU驱动对CPU的需求。满负载的话,G72MP12就需要一个A73跑满2.5Ghz且很难均衡负载(OpenGL ES的限制),而低端芯片只需一个A53就轻松完成。这里面大核小核,4核8核又造成了非常大的面积和功耗区别。
所以,提升显示分辨率绝不仅仅是图像细致一些这么简单,提升一倍的话,系统成本和功耗基本也会上一大截。简单来说,显示分辨率决定了一个芯片的下限。
把GPU在系统中的基本角色介绍完,下面从设计GPU的角度来分析。
想要做好一款GPU,先要分析市场。GPU市场主要有四大块:桌面和游戏机(3亿颗以下),手机和平板(20亿颗以下,其中近15亿颗被高通和苹果占住),电视和机顶盒(2亿颗以下),汽车面板和自动驾驶(小于1亿颗)。其中桌面和游戏机,自动驾驶暂不考虑,其他几类需求如下:
手机和平板:显示分辨率1080p到2K,4-8层图层,3D性能从弱到强,功耗2.5瓦,成本敏感。
电视和机顶盒:显示分辨率1080p到8K,8层图层,3D性能弱,功耗2.5瓦,成本敏感,需要画质增强。
汽车面板:显示分辨率1080p到2K,4层图层,3D性能弱,功耗2.5瓦,成本较敏感,不太需要汽车安全设计。
由于Vulkan成为安卓的下一代图形接口,固定图形流水线设计必将退出舞台,通用图形处理器,也就是所谓的GPGPU,成为必然趋势。
分辨率的变化可以提炼为可配置多核设计,UI和游戏的不同需求可提炼为大小核的设计。这里,大小核代表着同样填充率下不同的计算能力。两者结合,以期达到最高能效比和面积比。说到大小核,自然就衍生出一个问题,有没有必要像CPU那样在一个芯片内集成GPU的大小核?答案是否定的。CPU大小核之所以有用,是因为能效比会有4到5倍的差别,以及单线程性能的硬需求。而一个好的GPU设计,大小核无论跑UI还是图形,由于存在天然的多线程属性,同样的性能所消耗的能量应该是一致的。大小核面积会有差别,但是即使某段时间只用UI,不用计算能力,也得把计算能力放在芯片里,所以小核的意义就不大了,除非就是以UI为主要应用场景的GPU,不追求3D性能。
渲染方式上,目前主要有即时渲染和块渲染,这个话题已经有些年头了。前者是按照图元为基准,渲染相关顶点,几何,像素,然后合成输出。后者是以像素为基准,选取相关顶点和三角形,计算覆盖关系,最终合成输出。初一看,块渲染似乎更经济,因为它可以计算像素覆盖关系,避免重复渲染。但是反过来,块渲染时所需的三角形,顶点,属性和Varying信息,都是需要从内存读取的。如果存在大量的三角形,就需要多次重复读取,很可能省掉的带宽还不如用即时渲染。所以,决定哪个方式更优,关键在于顶点和像素的比例。就目前手机上的应用看来,像素远大于三角形或者顶点数量,这个比例大致在50:1到30:1。这时,用块渲染就更适合嵌入式设备。
从计算密度看,即时渲染的GPU面积一定小于块渲染的GPU,但是增加了带宽,变相增加了手机成本。至于增加的功耗,未必会比块渲染方式多。所以定性的讨论还是不够的,需要经过定量计算才能确定。
有个例子:某即时渲染的GPU A,填充率7200M p/s,曼哈顿3.0的跑分是25,T28nm下运行在450Mhz,功耗2.3瓦,面积13平方毫米,带宽12.8GB/s。
对应的,块渲染的GPU B,填充率1300M p/s,曼哈顿3.0跑4.5,T28下运行在650Mhz,功耗0.55瓦,面积4.8平方毫米,带宽688MB/s。
作为比较,填充率和曼哈顿3.0跑分比例一致,两个GPU都相差5.5倍。GPU A功耗低了30%,面积只有一半,但是带宽却是3.4倍。
这3倍多的带宽,几乎占了1.5个DDR4通道,并一下子带来了2瓦的功耗(28纳米),之前的GPU自身功耗优势当然无存,哪怕面积小一半也无济于事。
反过来,如果按照GPU B的绝对性能,那GPU A其实只需要2.3GB/s的带宽,虽然很大,却远不到一个DDR4通道最大带宽,同时功耗也很低,达不到2.5瓦的功耗上限,还能省一半面积,何乐而不为呢?
由此可以得出结论,低端手机完全可以用即时渲染的GPU,而中高端上还是得使用块渲染的GPU。随着工艺进步,即时渲染的GPU适用范围会更广。这应该出乎很多人的意料。
接着我们来看看图形渲染的流程:
每一步的过程不具体解释,我们关心的是哪些可以用通用计算单元做,哪些还是要固化为硬件做,这和性能面积功耗强相关。我们把上图流水对应到Mali GPU上,如下图:
把着色器更细化一些,如下图:
其中,顶点和像素的处理,计算量相对大,算法相对变化大,可以用通用的着色器,也就是上图中的执行单元Execution Engine。
曲面细分模块Tessellation,由于并不是必须的,在Mali的Norr中,并没有对应的硬件模块,可以用软件使用着色器通用处理单元来做。
深度和模板的测试以及合成,这些都属于像素的后处理,可以用专用硬件直接做,因为操作简单,计算量也和像素线性相关。
材质需要一个额外的单元来做,因为材质有许多专用的操作,而且每个像素点的输出都需要材质单元参与,输出线性相关。此外材质访存带宽也很大,所以拥有自己的访存单元,不占用着色器的存取单元。
在顶点和像素计算中用来传递数据的属性和Varying,需要根据顶点的属性数据,读取数据,插值计算像素的值,提供给像素着色器。计算量和顶点或者像素线性相关,适合固化为硬件单元。
根据图元的顶点位置信息,计算转换后的坐标与法向量,如果在边界之外或者在背面,那就不用输出,直接扔掉,节省带宽。这步就是背面剔除Culling和裁剪Clipping,可以用专用模块配合通用计算单元办到。最后剩下的有效部分,可以用来生成三角形列表,并增加到基于块状像素的队列中去,以便于光栅化。
接下去就是光栅化。这一步中涉及到深度和颜色等的计算,用通用着色器来做。具体做的时候,还需要一个硬件的三角形设置模块,对于某一块像素区域所涉及的三角形,读取上一步中形成的三角形列表,计算三角形每条边所对应方程的参数,以及平面和重心。由于和三角形输出率相关,算法固定,所以也适合固化。
此外,还需要一些额外的单元,来处理系统相关的事务,比如内存子系统,内部总线,以及负责管理任务和线程的模块。其中,任务管理模块又可以在不同层面细分,是一个GPU设计的精华所在。
以Mali为例,在最上层,以图元,顶点和像素为基准,把所有的任务都划分成三类Job,交给不同的处理单元,也就是Tiler,顶点/像素着色器。下一层,按照像素块为单元,又可以把整个屏幕分成很多像素点,也就是线程。在同一瞬间,每个着色器上都可以跑多个线程,这些线程的组合又被称作一个Warp。而提高计算密度的秘密,就在于在一个核内塞入更多的线程数。
再下层,具体到每一个着色器里面的执行引擎,每个时钟的输入是一个Clause。Clause就是某段没有分支的程序,当所有的输入数据都已经从内存读取并存放于寄存器后,这段程序会被无间断执行,直到结果输出。当输入数据还未取到,那么就切换到另一个准备就绪Clause。
所有这些任务,Warp和Clause的管理,需要有专门的硬件来做,以保持整个流水线利用率的最大化。
总之,只要是一直出现在图形流水上的工序,操作固定,计算量稳定,就可以用专用硬件单元来做,并不会浪费,相反还更省功耗面积。而计算变化较大的部分,就可以交给通用计算单元。
确定了通用和专用单元,接下来需要优化渲染流程,并调整硬件。先看顶点计算,如上图。在生成三角形列表的时候,有些三角形代表着背面,那只要算出法向量,那么就可以直接确定是否抛弃,省掉输出。
更进一步,如果根据顶点的远近关系,做某些简单计算,直接可以得出某些三角形被完全覆盖,那么也可以直接抛弃。在之后的像素渲染以及合成中,可以完全省掉处理。这被称作Early-Z。不过这还存在一个限制,在多个绘制函数Draw call中,如果命令被先后发送到GPU,不同函数间不容易做early-Z优化,因为如果要保留上一个同一区域绘制函数的结果一起做优化,可能需要花更大的代价。但是其块渲染的任务却可以等到所有绘制函数都完成后再开始。在这种情况下,从后往前画的绘制函数区,就不容易优化,而从前往后画的直接就能完成覆盖。这被称为forward killing,需要图形引擎预先计算出物体的深度信息,在生成脚本阶段就做好预判,提高渲染效率。
以上优化并不能解决所有的三角形覆盖问题,还可以再进一步。在像素渲染阶段,得到像素点对应的三角形以及深度信息后,一样可以抛弃被遮住的三角形,只计算被看到的那个三角形的颜色和光照,纹理,同样也免了后续的混合。这一步中的操作被称为TBDR,延迟块渲染。这是可以甚至是跨越Draw call的。如果顶点数足够少,被覆盖的三角形足够多,Early-Z和TBDR可以极大的减少像素渲染计算量。
在合成阶段,我们还可以做一个优化,就是在输出最终的块内容时,对整个区域计算一个CRC值.如果是16x16的块,其CRC大小通常只有1%,1080p的屏幕是100K字节左右。在下一次渲染时,我们再从DDR甚至内部缓存读出这个CRC值,来判断是不是内容有变化。如果没有,那直接放弃输出,节省带宽。不过,之前所做的渲染计算还是没法节省。
如果知道屏幕有一块区域在一段时间内不会有内容变化,那我们可以预先就告诉GPU,让它把这块区域从像素渲染里直接取消,从而免掉上一段中的计算。不过这需要和显示模块一起配合完成。
类似的渲染流水层面的优化还有很多,几乎每一步都能找出来。
在综合了所有的优化之后,我们终于做出了一个初始GPU,并得出其面积分布:
其中EE是计算引擎,面积47%,TEX是材质单元,面积20%。显然,我们优化的核心应该放在这两块。这其实又引出了图形处理器的一个奋斗目标:更高的计算密度。前面提到过,计算密度的定义,在以UI为主的低图形处理器上以像素输出率来衡量,在以游戏为主的高端上以浮点密度来衡量。
要提升UI像素输出密度,在合成单元能力固定的情况下,计算单元不重要,材质单元必须与输出能力匹配,一般是像素材质比1:2或者1:1。1:2的比例能做到两个材质点混合为一个后,配合一个像素输出,这在有些UI场景下很有用,提升了一倍的像素输出率。但是反过来,如果用不到,那多出来的材质单元面积就是浪费。具体是什么比例,只能见仁见智。
要提升浮点密度,方法也不难,就是堆运算单元,然后匹配上相应的图形处理器固化硬件,指令,缓存和带宽。
在具体设计运算单元的时候,还是有些考量的。之前,ARM一直使用SIMD+VLIW的结构。也就是说,以一个像素为一个线程,以其RGBA四个维度为矢量,形成一个32位数据的SIMD指令。然后,尽量找出可以并行的6个线程,放到一起并行。这6个线程分别是向量乘,向量加,标量乘,标量加,指令跳转还有查表。其实就是对应了运算单元的设计,如下图:
由于Mali是基于块渲染的,一个块内有16x16个像素,也就是256个线程,这些线程可以处于不同的程序段,有些在计算深度,有些在计算颜色。如果线程管理器能够一直找到这样的6个像素,对应不同的运算单元,把这些单元一直排满,那自然可以得到最高的单元利用率。可惜事与愿违,这样的高利用率场景并不好找,很多的时候是只有矢量单元被用上了,其余的都空着。
于是,Mali画风一转,把上图的标量乘和加去掉,且放弃VLIW,从而把指令跳转单元抽出来。最后形成了一个新的处理单元,如下图:
这里,一个128位宽的乘加和同样宽度的加法单元承担了之前标量和向量乘加,而查表运算也和加法单元混合在一起。和之前完全不同的是,这里的输入始终是128位宽的单个指令,而不是VLIW的6条指令,从而提高计算单元利用率。每时钟周期进来的数据,只能到FMA和ADD/TBL单元中的一个,没法同时进去,某种程度上减少了面积的有效利用率。
为了配合这一设计,Mali还做出了一个新的调整,如上图。由之前的按照像素点的RGBA四通道的矢量运算方式,改成四个像素各抽取一个颜色通道,塞到上面的FMA,同时运行四线程。由于大部分情况下,一个块上的256个像素的相同通道总是做一样的计算,保证了这个设计的高利用率。如果相邻四个点每个通道的运算都不一样,那效率自然会降低。
有些GPU还有另外一种运算单元形式:
在这里,乘加被放在小单元,比例更高;大单元除了乘加,还有查表,比例低;跳转单元单独放。这样可以使得计算单元比例更合理,面积利用率更高。
确定了计算单元的能力之后,有没有一个统一的方法,来精细的调整每个配套模块的比例呢?答案是有,先确定跑分标准,然后细化成子测试,最后在模型上统计出来:
目前移动上比较流行的标准是GFXBench,主流的有三个版本,2.x,3.x和4.x。每一个版本都有侧重,比如2.0侧重三角形生成,3.0侧重计算单元与材质,4.0侧重计算。Antutu也是一个标准,目前侧重阴影和三角形生成率。有时候,芯片和手机公司还会统计出主流的游戏,在芯片或者仿真平台甚至模型上跑,以期得到下一代GPU对计算能力的需求。
定下了标准跑分,接下去就是细化成更小的目标,是三角形,顶点,像素,材质,ZS,Varying,混合还是带宽要求。然后,在模型上,把这些细化需求翻译成PPA,给到每一个小模块,看看是不是还有压缩的空间。这是最底层的优化。
经历过上面的打磨之后,我们得到了一个更好的GPU。那是不是就没什么好改进了呢?还不够。从应用角度还是不停地有需求进来:
DRM,数据压缩, 系统硬件一致性,统一内存地址,AR/VR/AI,CPU驱动。
首先,是版权保护,播放有版权的内容时,解密和解码都是在保护世界完成的,而UI的操作可能需要GPU的参与。这块在安全篇中有论述,此处不再展开。
第二,所有的媒体和材质数据都可以进行压缩,以节省系统带宽和成本。上文讨论过,此处不再展开。
第三,系统双向硬件一致性问题。要实现GPU和CPU以及加速器之间数据互访而不用拷贝和刷新缓存,就需要支持双向一致性的总线,比如CCI550,这在基础篇已经讨论。最新的OpenCL2.0和Vulkan都支持这一新的特性。如下图,在数据交互非常频繁地情况下,可以节省30%甚至90%左右的运行时间。
不幸的是,由于OpenGL ES天生就不支持这个特性,所以对于目前绝大多数的图形应用,哪怕接了CCI550,GPU也是不会发出任何带有监听操作的传输的。这种情况到了Vulkan以后会有改善。
第四,和CPU的统一物理地址。在桌面上,CPU和GPU的访问空间是完全独立的,而移动处理器从开始就统一了物理地址。当然,他们的页表还是分开的。在基础篇我们就讲过,统一的好处就是省带宽和成本,恰好块渲染的GPU的特点就是带宽相对较小。剩下的只要把总线和内存控制器的调度做好,保持内存带宽的利用率在一个相对较高的水平就行。
硬件一致性和统一地址有一个应用就是异构计算,CPU/GPU/DSP/加速器均可使用相同物理地址,并且硬件自动做好一致性维护。具体的计算可以是图像,也可以是语音。不过很可惜,在高端手机上,如果把所有的处理单元都跑起来,那功耗肯定是远高于2.5瓦的,甚至可以到10瓦。而且受限于GPU的软件,双向硬件一致性也没有得到广泛应用,目前最多是做一些ISP后处理。
第五,AI。在AI篇我们提到。如果AI跑在GPU,那肯定需要支持INT8甚至更小的乘加操作,这对于GPU没有任何问题。不过,AI更需要的参数压缩,Mali的GPU并没有原生支持。这样一来,本来的四核MP4差不多是用2个128位AXI接口,只能提供32GB/s左右的读带宽。不压缩的话,也就能支持64GB INT8的计算量,远小于GPU四核一般在1Tops的INT8计算量。
第六,VR。VR对于GPU来说有相当大的关系。首先,由于左右眼需要分别渲染,并且分辨率需要4K以上,这就对GPU性能提出了1080p时8倍的需求,对系统带宽也是一种考验。细分下来,有这样一些需求:
左右眼独立渲染:如下图,VR场景中的三角形或者顶点部分,左右眼是共享的,但是旋转和之后的像素处理,必须是分开的。由于顶点渲染只占整个工作量的10%,所以能节省的计算量相当有限,只是可以减少些顶点材质的读取。
畸变矫正:这个可以在顶点渲染最后加一步矩阵乘法,轻易做到。不过,由于这个操作夹杂在顶点和像素渲染之间,所以还是需要额外的API来提醒硬件。
异步时间扭曲ATW:其原理是当发现计算下一帧需要的时间超出预期,索性就不去计算了,而是把当前帧按照头部移动方向做一个插值,造一个假的图像。这样,就需要一个API,以估算下一帧生成时间,还需要一个额外的定时器,根据显示模块的Vsync信号计算剩余可用时间。如果时间不够,会直接拿取插值后的图像,而这个图像计算也在GPU,计算量小且优先级很高,保证赶上Vsync信号。
多视图渲染Multi-View:也就是对于视图的非焦点区域,使用低解析度,焦点区域,高解析度。要做到这点,使用高解析度,总的计算量可以降低一半左右。实际运用中,由于焦点区域的确定需要眼球跟踪,比较复杂,所以会采用中心区域来替代。
Front buffer:原来的桌面GPU设计中,显示缓冲分两块,front buffer和back buffer交替输出,当中用Vsync做同步,按帧输出。现在只使用一块front buffer,以hsync为同步标志,按行输出,这样,整个帧的渲染时间并不变,但是粒度变细。对于GPU来说,这需要加入按行渲染的次序关系。这和上一个Multi-View原理并不相同,前一个虽然分成了多视图区域,但是并不规定渲染次序,块与块之间还是乱序的,只不过最后输出的时候都是渲染完成好的。而Front buffer相当于在行与行间插入了一个同步指令,如果纯粹交由硬件来调度,很可能会降低性能。也可以GPU的frame buffer保持原样,用显示模块来做这个事情,更容易实现。
第七,AR。在AI篇中,我们提到,GPU其实也在做渲染,没有什么特殊的需求,除非把识别的工作交给GPU来做。
还有很重要的一点,就是GPU驱动对CPU造成的负载。在OpenGL ES上,由于API本身的限制,很多驱动任务必须是一个线程内完成的。这就要求必须在一个CPU核上跑。GPU核越多,单个CPU核的负载越高。在Mali G71之前,差不多10-12个900Mhz的GPU核就需要一个跑在2.5Ghz的A73来负责跑驱动,其他的大核小核再多都帮不上忙。但事实上,由于大核的能效比小核差了4到5倍,所以一定是小核来跑驱动更省电。反过来,如果使用了更多的GPU核,那最后的瓶颈会变成单个CPU的性能,而不是GPU。解决的方法有几个,一是使用Vulkan。Vulkan在设计阶段就考虑到了这个问题,天然支持CPU多线程的负载均衡,能很好的解决这个问题。我曾经看到过一款桌面GPU,在16核A53的服务器上只发挥出x86服务器上的30%性能,而从Open GL换成Vulkan后,才把瓶颈转移到GPU自身。
不过,虽然Vulkan已经是谷歌钦定的下一代图形接口,基于Vulkan的图形应用流行可能还需要3-5年时间。另外一个可行方法就是优化GPU驱动软件本身,并把一部分的软件工作交给硬件来完成,比如内存管理模块的硬件化,还可以使用一个MCU和内嵌缓存来翻译和处理命令序列,替代CPU的工作。这个MCU可以只跑在几百兆,功耗十几毫瓦,远低于小核的100多毫瓦,更低于大核的几百毫瓦。
最后,做出来的GPU在PPA上还得和高通的Adreno对比(苹果的GPU在计算密度上也不如高通)。目前世界上所有的块渲染的GPU中,高通是能效比和性能密度最好的,比最新的Mali GPU高30%以上。其中,有几个地方是Mali难以弥补的,比如系统缓存,针对少数几个配置的前端优化(Mali需要兼顾1-32核),以及确定的后端制程和优化。这里的每一项都可以提供5%-10%的优化空间,积累起来也是不小的优势。
总之,GPU的设计是一个不断细化的过程,选好大方向,把标准跑分明确,再注意新的趋势和需求,把模型和验证流程跑熟,剩下的就是不断打磨了。
原文转载自: https://zhuanlan.zhihu.com/c_70349842