龙芯性能堕落到和arm a53性能怎样比了吗

当前位置浏览文章
参考链接Android jniLibs下目录详解(.so文件)armeabi-v7a armeabi arm64-v8a关于Android的.so文件你所需要知道的思维导图一、cpu三大架构:MIPS、ARM、X861. 用途分布:armeabi系列:属于ARM (A7、A9、A15、A53、A57) 包含:高通、麒麟(华为海思)、澎湃(小米)、联发科、猎户座(三星Exynos)mips系列:属于MIPS ,多用在网关、猫、机顶盒等。代表:中国“龙芯”x86系列:pc模拟器、Intel Atom系列处理器(英特尔放弃应用于手机、PC、平板以及可穿戴设施的Atom处理器)2. 详细分类:arm:armeabi(arm v5 cpu 32位)、armeabi-v7a(针对有浮点运算或高级扩展功能的arm v7 cpu 32位)、arm64-v8a(同时包含着32位的ARMv7和64位的ARMv8两种架构)mips:mips(32位)、mis64(64位)x86:x86(32位)、x86_64(64位)3. 架构与目录的对应关系二、应用1. 错误分析UnsatisfiedLinkError:找不到对应的so包分析:很多设施都支持多于一种的ABI。例如ARM64和x86设施也可以同时运行armeabi-v7a和armeabi的二进制包。当一个应用安装在设施上,只有该设施支持的CPU架构对应的.so文件会被安装。在x86设施上,libs/x86目录中如果存在.so文件的话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件(因为x86设施也支持armeabi-v7a和armeabi)。举例:现有一台x86设施,有a,b,c三个so文件,x86只放置a,b,其余目录都放a,b,c。则在安装apk时会安装x86目录下的so文件,即a,b,没有c。则在运行到c功能时会报UnsatisfiedLinkError错误。解决办法:尽可能的提供专为每个ABI优化过的.so文件,但要么全部支持,要么都不支持:你不应该混合着使用。你应该为每个ABI目录提供对应的.so文件。2. 开发技巧查看cpu信息三、疑问1. 只提供armeabi架构的.so文件而忽略其他ABIs来减小apk大小?所有的x86/x86_64/armeabi-v7a/arm64-v8a设施都支持armeabi架构的.so文件,因此似乎移除其他ABIs的.so文件是一个减少APK大小的好技巧。但事实上并不是:这不只影响到函数库的性能和兼容性。x86设施能够很好的运行ARM类型函数库,但并不保证100%不发生crash,特别是对旧设施。64位设施(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。如有问题,麻烦指出ARM杂谈ARM杂谈关注专栏更多SoC{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\u002Fpay.zhihu.com\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&title&:&ARM攒机指南-开篇&,&author&:&zhong-zou-ci-jian-lu&,&content&:&\u003Cp\u003E经常有人说,现在做手机芯片就像搭积木,买点IP,连一下,后端外包。等芯片回来,上电,起操作系统,大功告成。这么简单,要不我们也来动手攒一颗吧。不过在攒机之前,我们还是先要把基础概念捋顺了。\u003C\u002Fp\u003E\u003Cp\u003E评价一颗芯片,着眼点主要是功能,性能,功耗和价格。功能,是看芯片内部有什么运算模块,比如处理器,浮点器,编解码器,数字信号处理器,图形加速器,网络加速器等,还要看提供了什么接口,比如闪存,内存,PCIe,USB,SATA,以太网等。\u003C\u002Fp\u003E\u003Cp\u003E性能,对CPU来说就是基准测试程序能跑多少分,比如Dhrystone,Coremark,SPEC06等。针对不同的应用,比如手机,还会看图形处理器的跑分,而对网络处理器,会看包转发率。当然,还需要跑一些特定的应用程序,来得到更准确的性能评估。\u003C\u002Fp\u003E\u003Cp\u003E功耗,就是在跑某个程序的时候,芯片的功率是多少瓦。通常,这时候处理器会跑在最高频率,但这并不意味着所有的晶体管都在工作,由于power gating和clock gating的存在,有些没有被用到的逻辑和片上内存块并没在耗电。芯片公司给出的处理器功耗,通常都是在跑Dhrystone。这个程序有个特点,它只在一级缓存之上运行,不会访问二级缓存,更不会访问内存。这样得出的功耗,其实并不是包含了内存访问的真实功耗,也不是最大功耗。为得到处理器最大功耗,需要运行于一级缓存之上的向量和浮点指令,其结果通常是Dhrystone功耗的2-3倍。但是从实际经验看,普通的应用程序并不能让处理器消耗更高的能量,所以用Dhrysone测量也没什么问题。当然,要准确衡量整体的芯片功耗,还得考虑各种加速器,总线和接口,并不仅仅是处理器。\u003C\u002Fp\u003E\u003Cp\u003E在芯片设计阶段,功能,性能,功耗和价格就转换成了PPA。PPA指的是性能,功耗和面积。其中,性能有两层含义。在前端设计上,它表示的是每赫兹能够跑多少标准测试程序分。设计处理器的和时候,会有多少级流水线的说法。通常来说,流水线级数越多,芯片能跑到的最高频率越高。可是并不是频率越高,性能就越高。这和处理器构架有很大关系。典型的反例就是Intel的奔腾4,30多级流水,最高频率高达3G赫兹,可是由于流水线太长,一旦指令预测错误,重新抓取的指令要重走这几十级流水线,代价是很大的。而它的指令又非常依赖于编译器来优化,当时编译器又没跟上,导致总体性能低下。而MIPS或者PowerPC的处理器频率都不高,但是每赫兹性能相对来说还不错,总体性能就会提高一些。所以性能要看总体跑分,而不是每赫兹跑分。\u003C\u002Fp\u003E\u003Cp\u003E性能的另外一个含义就是指最高频率,这是从后端设计角度来说的。通常后端的人并不关心每赫兹能达到多少跑分,只看芯片能跑到多少频率。频率越高,在每赫兹跑分一定的情况下,总体性能就越高。请注意对于那些跑在一级缓存的程序,处理器每赫兹跑分不会随着频率的变化而变化。而如果考虑到多级缓存,总线和外围接口,那肯定就不是随处理器频率线性增加了。\u003C\u002Fp\u003E\u003Cp\u003E哪些因素会影响频率?就算只从后端角度考虑,因素也很多,以下方面仅供参考。\u003C\u002Fp\u003E\u003Cp\u003E首先,受工艺的影响。现在先进的半导体工厂就那么几家,Intel,台积电,三星,格芯,联电等。拿台积电来说,它之前提供16纳米的工艺,其中还分了很多小结点,比如FFLL++和FFC。每个小节点各有特点,有些能跑到更高频率,有些功耗低,有些成本低。在不同的工艺上,芯片能跑的最高频率不同,功耗和面积也不同。\u003C\u002Fp\u003E\u003Cp\u003E其次,受后端库的影响。台积电会把工艺中晶体管的参数抽象出来,做成一个物理层开发包,提供给工具厂商,IP厂商和芯片厂商。而这些厂商的后端工程师,就会拿着这个物理层开发包,做自己的物理库。物理库一般包含逻辑和内存两大块。根据晶体管参数的不同,会有不同特性,适合于不同的用途。而怎么把这些不同特性的的库,合理的用到各个前端设计模块,就是一门大学问。一般来说,源极和漏极通道越短,电子漂移距离越短,能跑的频率就越高。可是,频率越高,动态功耗就越大,并且可能是按指数级上升。除此之外,还会有Track这种说法,指的是的标准单元的宽度。宽度越大,电流越大,越容易做到高频,面积也越大。还有一个可调的参数就是阈值电压,决定了栅极的电压门限,门限越低,频率能冲的越高,静态功耗也越大,按对数级上升。\u003C\u002Fp\u003E\u003Cp\u003E接下来,受布局和布线的影响。芯片里面和主板一样,也是需要多层布线的,每一层都有个利用率。总体面积越小,利用率越高,布线就越困难。而层数越多,利用率越低,成本就越高。在给出一些初始和限制条件后,EDA软件会自己去不停的计算,最后给出一个可行的频率和面积。\u003C\u002Fp\u003E\u003Cp\u003E再次,受前后端协同设计的影响。处理器的关键路径直接决定了最高频率。ARM的大核,A73之后,由于采用了虚地址索引VIPT,免去了查MMU,关键路径已经集中到一级缓存的访问时间延迟上了。\u003C\u002Fp\u003E\u003Cp\u003E从功耗角度,同样是前后端协同设计,某个访问片上内存的操作,如果知道处理器会花多少时间,用哪些资源,就可以让内存的空闲块关闭,从而达到省电的目的。这种技巧可能有上千处,只有自己做处理器才会很清楚。\u003C\u002Fp\u003E\u003Cp\u003E再往上,就是动态电压频率缩放DVFS。这里需要引入功耗的组成概念。芯片功耗分成动态和静态两部分,静态就是晶体管漏电造成的,大小和芯片工艺,晶体管数,电压相关,而动态是开关切换造成的,所以和晶体管数,频率,电压相关。控制动态功耗的方法是clock gating,频率变小,自然动态功耗就小。控制静态功耗的方法是power gating,关掉电源,那么静态和动态功耗都没了。还可以降低电压,那么动态功耗和静态功耗自然都小。可是电压不能无限降低,否则电子没法漂移,晶体管就不工作了。并且,晶体管跑在不同的频率,所需要的电压是不一样的,拿16纳米来说,往下可以从0.9V变成0.72V,往上可以变成1V或者更高。别小看了这一点点的电压变化,动态功耗的变化,是和电压成2次方关系,和频率成线性关系的。而频率的上升,同样是依赖于电压提升的。所以,1.05V和0.72V,电压差了45%,动态功耗可以差3倍。\u003C\u002Fp\u003E\u003Cp\u003E再往上,就是软件电源管理了。芯片设计者把每个大模块的clock gating和power gating进行组合,形成不同的休眠状态,软件可以根据温度和运行的任务,动态的告诉处理器每个模块进入不同的休眠状态,从而在任务不忙的时候降低功耗。\u003C\u002Fp\u003E\u003Cp\u003E从上面我们可以看到,功耗和性能其实是相辅相成的。芯片设计者可以用不同的工艺和物理库,在给定功耗下,设计出最高可运行频率,然后用软件动态控制芯片运行频率和电压,优化功耗。\u003C\u002Fp\u003E\u003Cp\u003E频率和面积其实也是互相影响的。给定一个目标频率,选用了不同的物理库,不同的track,不同的利用率,形成的芯片面积就会不一样。通常来说,越是需要跑高频的芯片,所需的面积越大。频率差一倍,面积可能有百分之几十的差别。别小看这百分之几十,对晶体管来说,面积就是成本,晶圆的总面积一定,价钱一定,那单颗芯片的面积越小,成本越低,并且此时良率也越高。\u003C\u002Fp\u003E\u003Cp\u003E芯片成本除了流片,晶圆和封测费,还来自于授权费,工具费,运营开销等,通常手机处理器这样复杂的芯片,没有几千万美元是不可能做出来的。就算做出来,没有卖掉几百万片,也肯定是亏本的。\u003C\u002Fp\u003E\u003Cp\u003E这里再提下ARM的大小核设计。其最初的目的是想设计两组核,小核每赫兹性能低,面积小,跑在低频;大核每赫兹性能高,面积大,跑在高频。运行简单任务,大核关闭,小核在低频,动态和静态功耗都低,而大核用高频运行复杂任务。小核在低功耗场景下,通常只需要大核一半的面积和五分之一的功耗。这和不区分大小核,单纯调节电压频率比,有显著优势。\u003C\u002Fp\u003E\u003Cp\u003E那为什么不让小核跑在高频运行复杂任务呢?理论上,由于每赫兹性能低,对于相同的任务,小核必须跑在比大核更高的频率才能完成,这就意味着更高的电压。此时,动态功耗占上风,并且和电压成三次方关系,最终的功耗会高出大核不少。此外,我们前面已经解释过,小核要跑在高频,面积会增大不少,可能比大核还要大。所以,这里存在一个平衡点。拿A53\u002FA57在28纳米上举例,当它们跑在1.2Ghz的时候,功耗可能差两倍,性能却只差50%。而继续升频,功耗3次方上升,性能线性上升,最终可能在2Ghz达到平衡点。此时,A53的能效比反而不如A57。当然,这个平衡点在不同工艺上是不断变化的。再反过来考虑,在2Ghz之前,其实可以用高频A53做大核,能效比并不低于A57。事实上,很多手机芯片已经这么做了。\u003C\u002Fp\u003E\u003Cp\u003E还有一个问题,既然小核能效比更高,那为什么不用多个小核来代替大核呢?这是因为手机上的很多应用,如果没有特别优化,都是单线程的,多线程编程向来容易出问题。此时,多个小核并不能代替一个大核,所以大核必须存在。而当应用适合分成多线程,也没有过多同步的开销时,毫无疑问,小核更具能效比。\u003C\u002Fp\u003E\u003Cp\u003E从上面我们看到,设计芯片很大程度上就是在平衡。影响因素,或者说坑,来自于方方面面,IP提供商,工厂,市场定义,工程团队。水很深,坑很大,没有完美的芯片,只有完美的平衡。在这点上,苹果是一个很典型的例子。苹果A10的CPU频率不很高,但是Geekbench单核跑分却比 A73高了整整75%,接近Intel桌面处理器的性能。为什么?因为苹果用了大量的面积换取性能和功耗。首先,它使用了六发射,而A73只有双发射,流水线宽了整整三倍。当然,三倍的发射宽度并不表示性能就是三倍,由于数据相关性的存在,发射宽度的效益是递减的。再一点,苹果使用了整整6MB的缓存,而这个数字在别的手机芯片上通常是2MB。对一些标准跑分,比如SpecInt06,128KB到256KB二级缓存带来的性能提升仅仅是7%左右,而256KB到1MB带来的提升更小,缓存面积却是4倍。第三,除了一二三级缓存之外,苹果大量增加处理器在各个环节的缓冲,比如指令预测器等。当然,面积的提升同样带来了静态功耗的增加,不过相对于提升频率,造成动态功耗增加来说,还是小的。再次,苹果引入的复杂的电源,电压和时钟控制,虽然增加了面积,但由于系统软件都是自己的,可以从软件层面就进行很精细的优化,将整体功耗控制的非常好。举个例子,Wiki上面可以得知,A10上的大核Hurricane面积在TSMC的16nm上是4.18 平方毫米,而ARM的下一代大核,在2.4Ghz时,SPECINT2000跑分接近,面积少了70%。但是,也只有苹果能这么做,一般芯片公司绝对不会走苹果这样用大量面积换性能和功耗的路线,那样的话毛利就太低了。\u003C\u002Fp\u003E&,&updated&:new Date(&T06:53:32.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:23,&likeCount&:124,&state&:&published&,&isLiked&:false,&slug&:&&,&isTitleImageFullScreen&:false,&rating&:&none&,&sourceUrl&:&&,&publishedTime&:&T14:53:32+08:00&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&url&:&\u002Fp\u002F&,&titleImage&:&&,&summary&:&&,&href&:&\u002Fapi\u002Fposts\u002F&,&meta&:{&previous&:null,&next&:null},&snapshotUrl&:&&,&commentsCount&:23,&likesCount&:124},&&:{&title&:&ARM攒机指南-基础篇&,&author&:&zhong-zou-ci-jian-lu&,&content&:&\u003Cp\u003E在开篇里,我们对芯片PPA有了初步的认识。下面,让我们从访存这个简单的问题开始展开介绍芯片基础概念。\u003C\u002Fp\u003E\u003Cp\u003ECPU是怎样访问内存的?简单的答案是,CPU执行一条访存指令,把读写请求发往内存管理单元。内存管理单元进行虚实转换,把命令发往总线。总线把命令传递给内存控制器,内存控制器再次翻译地址,对相应内存颗粒进行存取。之后,读取的数据或者写入确认按照原路返回。再复杂些,当中插入多级缓存,在每一层缓存都未命中的情况下,访问才会最终达到内存颗粒。\u003C\u002Fp\u003E\u003Cp\u003E知道了完整的路径,就可以开始研究每一步中的硬件到底是怎么样的,读写指令到底是怎样在其中传输的。首先要说下处理器。处理器的基本结构并不复杂,一般分为取指令,译码,发射,执行,写回五个步骤。而这里说的访存,指的是访问数据,不是指令抓取。访问数据的指令在前三步没有什么特殊,在第四步,它会被发送到存取单元,等待完成。当指令在存取单元里的时候,产生了一些有趣的问题。\u003C\u002Fp\u003E\u003Cp\u003E第一个问题,对于读指令,当处理器在等待数据从缓存或者内存返回的时候,它到底是什么状态?是等在那不动呢,还是继续执行别的指令?一般来说,如果是乱序执行的处理器,那么可以执行后面的指令,如果是顺序执行,那么会进入停顿状态,直到读取的数据返回。当然,这也不是绝对的。在举反例之前,我们先要弄清什么是乱序执行。乱序执行是指,对于一串给定的指令,为了提高效率,处理器会找出非真正数据依赖的指令,让他们并行执行。但是,指令执行结果在写回到寄存器的时候,必须是顺序的。也就是说,哪怕是先被执行的指令,它的运算结果也是按照指令次序写回到最终的寄存器的。这个和很多程序员理解的乱序执行是有区别的。有些人在调试软件问题的时候,会觉得使用了一个乱序的处理器,那么可能会使得后面的代码先被执行,从而让调试无法进行。这搞混了两个个概念,就是访存次序和指令完成次序。对于普通的运算指令,他们仅仅在处理器内部执行,所以程序员看到的是写回或者完成次序。而对于访存指令,指令会产生读请求,并发送到处理器外部,看到的次序是访存次序。对于乱序处理器,可能同时存在多个读写请求,而其次序,如果不存在相关性,可以是打乱的,不按原指令顺序的。但是与这些读写指令无相关性的的运算指令,还是按照乱序执行,顺序提交的。\u003C\u002Fp\u003E\u003Cp\u003E对于顺序执行的处理器,同样是两条读指令,一般必须等到前一条指令完成,才能执行第二条,所以在处理器外部看到的是按次序的访问。不过也有例外,比如读写同时存在的时候,由于读和写指令实际上走的是两条路径,所以可能会看到同时存在。还有,哪怕是两条读指令,也有可能同时存在两个外部请求。比如Cortex-A7,对于连续的读指令,在前一条读未命中一级缓存,到下一级缓存或者内存抓取数据的时候,第二条读指令可以被执行。所以说,乱序和顺序并不直接影响指令执行次序。而乱序需要额外的缓冲和逻辑块(称为重排序缓冲,\u003Cbr\u003Ere-order buffer)来计算和存储指令间的相关性以及执行状态,顺序处理器没有重排序缓冲,或者非常简单。这些额外的面积可不小,可以占到处理器核心的40%。它们带来更高的并行度,性能提升却未必有40%。因为我们写的单线程程序,由于存在很多数据相关,造成指令的并行是有限的,再大的重排序缓冲也解决不了真正的数据相关。所以对于功耗和成本敏感的处理器还是使用顺序执行。\u003C\u002Fp\u003E\u003Cp\u003E还有一点需要注意,顺序执行的处理器,在指令抓取,解码和发射阶段,两条或者多条指令,是可以同时进行的。比如,无依赖关系的读指令和运算指令,可以被同时发射到不同的执行单元,同时开始执行。并且,在有些ARM处理器上,比如Cortex-A53,向量或者加解密指令是可以乱序完成的,这类运算的结果之间并没有数据依赖性。这点请千万注意。\u003C\u002Fp\u003E\u003Cp\u003E再来看看写指令。写和读有个很大的不同,就是写指令不必等待数据写到缓存或者内存,就可以完成了。写出去的数据会到一个叫做store buffer的缓冲,它位于一级缓存之前,只要它没满,处理器就可以直接往下走,不必停止并等待。所以,对于连续的写指令,无论顺序还是乱序执行处理器,都可能看到多个写请求同时挂在处理器总线上。同时,由于处理器不必像读指令那样等待结果,就可以在单位时间内送出更多写请求,所以我们可以看到写带宽通常是大于读带宽的。\u003C\u002Fp\u003E\u003Cp\u003E对于同时存在的多个请求,有一个名词来定义它,叫做outstanding transaction,简称OT。它和延迟一起,构成了我们对访存性能的描述。延迟这个概念,在不同领域有不同的定义。在网络上,网络延迟表示单个数据包从本地出发,经过交换和路由,到达对端,然后返回,当中所花的总时间。在处理器上,我们也可以说读写的延迟是指令发出,经过缓存,总线,内存控制器,内存颗粒,然后原路返回所花费的时间。但是,更多的时候,我们说的访存延迟是大量读写指令被执行后,统计出来的平均访问时间。这里面的区别是,当OT=1的时候,总延时是简单累加。当OT&1,由于同时存在两个访存并行,总时间通常少于累加时间,并且可以少很多。这时候得到的平均延迟,也被称作访存延迟,并且用得更普遍。再精确一些,由于多级流水线的存在,假设流水线每一个阶段都是一个时钟周期,那访问一级缓存的平均延迟其实就是一个周期.而对于后面的二级,三级缓存和内存,就读指令来说,延迟就是从指令被发射(注意,不是从取指)到最终数据返回的时间,因为处理器在执行阶段等待,流水线起不了作用。如果OT=2, 那么时间可能缩短将近一半。OT&1的好处在这里就体现出来了。当然,这也是有代价的,存储未完成的读请求的状态需要额外的缓冲,而处理器可能也需要支持乱序执行,造成面积和功耗进一步上升。对于写指令,只要store\u003Cbr\u003Ebuffer没满,还是一个时钟周期。当然,如果流水线上某个节拍大于一个时钟周期,那平均的延时就会取决于这个最慢的时间。在读取二级,三级缓存和内存的时候,我们可以把等待返回看作一个节拍,那么就能很自然的理解此时的延迟了。由此,我们可以得到每一级缓存的延迟和访存延迟。\u003C\u002Fp\u003E\u003Cimg src=\&v2-a4842ebaa5ced2cf4bd899.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&811\& data-rawheight=\&548\&\u003E\u003Cp\u003E上图画了ARM某处理器读写指令经过的单元,简单流程如下:\u003C\u002Fp\u003E\u003Cp\u003E当写指令从存取单元LSU出发,它首先经过一个小的store queue,然后进入store buffer。之后,写指令就可以完成了,处理器不必等待。Store\u003Cbr\u003Ebuffer通常由几个8-16字节的槽位组成,它会对自己收到的每项数据进行地址检查,如果可以合并就合并,然后发送请求到右边的一级缓存,要求分配一行缓存,来存放数据,直到收到响应,这称作写分配write\u003Cbr\u003Eallocate。当然,等待的过程可以继续合并同缓存行数据。如果数据是Non-Cacheable的,那么它会计算一个等待时间,然后把数据合并,发送到总线接口单元BIU里面的写缓冲Write\u003Cbr\u003Ebuffer。 而写缓冲在把数据发到二级缓存之前,会经过监听控制单元,把四个核的缓存做一致性检测。\u003C\u002Fp\u003E\u003Cp\u003E当读指令从存取单元LSU出发,无论是否Cacheable的,都会经过一级缓存。如果命中,那么直接返回数据,读指令完成。如果未命中,那么Non-Cacheable的请求直接被送到Read Buffer。如果是Cacheable的,那么一级缓存需要分配一个缓存行,并且把原来的数据写出到替换缓冲eviction buffer,同时发起一个缓存行填充,发送到Linefill Buffer。Eviction\u003Cbr\u003Ebuffer会把它的写出请求送到BIU里面的Write buffer,和Store Buffer送过来的数据一起,发到下一级接口。然后这些请求又经过监听控制单元做一致性检测后,发到二级缓存。当然有可能读取的数据存在于别的处理器一级缓存,那么就直接从那里抓取。\u003C\u002Fp\u003E\u003Cp\u003E过程并不复杂,但程序员关心的是这个过程的瓶颈在哪,对读写性能影响如何。我们已经解释过,对于写,由于它可以立刻完成,所以它的瓶颈并不来自于存取单元;对于读,由于处理器会等待,所以我们需要找到读取路径每一步能发出多少OT,每个OT的数据长度是多少。\u003C\u002Fp\u003E\u003Cp\u003E拿Cortex-A7来举例,它有2x32字节linefill\u003Cbr\u003Ebuffer,支持有条件的miss-under-miss(相邻读指令必须在3时钟周期内),也就是OT最多等于2,而它的数据缓存行长度是64字节,所以每个OT都是半个缓存行长度。对于Cacheable的读来说,我还关心两个数据,就是eviction buffer和Write\u003Cbr\u003Ebuffer,它们总是伴随着line fill。在A7中,存在一个64字节的eviction buffer和一个Write buffer。有了这些条件,那么我就可以说,对于连续的读指令,我能做到的OT就是2,而linefill的速度和eviction,\u003Cbr\u003Ewrite buffer的速度一致,因为2x32=64字节。\u003C\u002Fp\u003E\u003Cp\u003E那这个结论是不是正确?写个小程序测试下就知道。我们可以关掉二级缓存,保留一级缓存,然后用以下指令去读取一个较大的内存区域。所有的地址都是缓存行对齐。不对齐,甚至越过缓存行边界,会把一个操作变成两个,肯定会慢。伪代码如下:\u003C\u002Fp\u003E\u003Cp\u003Eloop\u003C\u002Fp\u003E\u003Cp\u003Eload R0, addr+0\u003C\u002Fp\u003E\u003Cp\u003Eload R0, addr+4\u003C\u002Fp\u003E\u003Cp\u003Eload R0, addr+8\u003C\u002Fp\u003E\u003Cp\u003Eload R0, addr+12\u003C\u002Fp\u003E\u003Cp\u003Eaddr=addr+16\u003C\u002Fp\u003E\u003Cp\u003E这里通过读取指令不断地去读数据。通过处理器自带的性能计数器看了下一级缓存的未命中率,6%多一点。这恰恰是4\u002F64字节的比率。说明对于一个新的缓存行,第一个四字节总是未命中,而后面15个四字节总是命中。当然,具体的延迟和带宽还和总线,内存控制器有关,这里只能通过命中率简单验证下。\u003C\u002Fp\u003E\u003Cp\u003E对于有的处理器,是严格顺序执行的,没有A7那样的miss-under-miss机制,所以OT=1。我在Cortex-R5上做同样的实验,它的缓存行长度是32字节,2xLinefill buffer是32字节。测试得到的命中率是12%多点。也完全符合估算。\u003C\u002Fp\u003E\u003Cp\u003E但是为什么R5要设计两个32字节长度的Linefill\u003Cbr\u003Ebuffer?既然它的OT=1,多出来的一个岂不是没用?实际上它是可以被用到的,而方法就是使用预取指令PLD。预取指令的特点就是,它被执行后,处理器同样不必等待,而这个读请求会被同样发送到一级缓存。等到下次有读指令来真正读取同样的缓存行,那么就可能发现数据已经在那了。它的地址必须是缓存行对齐。这样,读也可像写那样把第二个\u003Cbr\u003E\u003Cbr\u003ELinefill buffer给用上了。\u003C\u002Fp\u003E\u003Cp\u003E我们把它用到前面的例子里:\u003C\u002Fp\u003E\u003Cp\u003Eloop\u003C\u002Fp\u003E\u003Cp\u003EPLD addr+32\u003C\u002Fp\u003E\u003Cp\u003Eload R0,\u003Cbr\u003E\u003Cbr\u003Eaddr+0;...;load R0, addr+28;\u003C\u002Fp\u003E\u003Cp\u003Eload R0,\u003Cbr\u003E\u003Cbr\u003Eaddr+32;...;load R0, addr+60;\u003C\u002Fp\u003E\u003Cp\u003Eaddr=addr+64\u003C\u002Fp\u003E\u003Cp\u003EPLD预先读取第二行读指令的地址。测试发现,此时的未命中率还是6%。这也符合估算,因为第二排的读指令总是命中,第一排的未命中率4\u002F32,平均下就是6%。而测试带宽提升了80%多。单单看OT=2,它应该提升100%,但实际不可能那么理想化,80%也可以理解。\u003C\u002Fp\u003E\u003Cp\u003E还有一种机制使得OT可以更大,那就是缓存的硬件预取。当程序访问连续的或者有规律的地址时,缓存会自动检测出这种规律,并且预先去把数据取来。这种方法同样不占用处理器时间,但是也会占用linefill\u003Cbr\u003E\u003Cbr\u003Ebuffer,eviction buffer和write buffer。所以,如果这个规律找的不好,那么反而会降低效率。\u003C\u002Fp\u003E\u003Cp\u003E读看完了,那写呢?Cacheable的写,如果未命中缓存,就会引发write\u003Cbr\u003E\u003Cbr\u003Eallocate,继而造成Linefill和eviction,也就是读操作。这点可能很多程序员没想到。当存在连续地址的写时,就会伴随着一连串的缓存行读操作。有些时候,这些读是没有意义的。比如在memset函数中,可以直接把数据写到下一级缓存或者内存,不需要额外的读。于是,大部分的ARM处理器都实现了一个机制,当探测到连续地址的写,就不让store\u003Cbr\u003E\u003Cbr\u003Ebuffer把数据发往一级缓存,而是直接到write buffer。并且,这个时候,更容易合并,形成突发写,提高效率。在Cortex-A7上它被称作Read allocate模式,意思是取消了write allocate。而在有的处理器上被称作streaming模式。很多跑分测试都会触发这个模式,因此能在跑分上更有优势。\u003C\u002Fp\u003E\u003Cp\u003E但是,进入了streaming模式并不意味着内存控制器收到的地址都是连续的。想象一下,我们在测memcpy的时候,首先要从源地址读数据,发出去的是连续地址,并且是基于缓存行的。过了一段时间后,缓存都被用完,那么eviction出现了,并且它是伪随机的,写出去的地址并无规律。这就打断了原本的连续的读地址。再看写,在把数据写到目的地址时,如果连续的写地址被发现,那么它就不会触发额外的linefill和eviction。这是好事。可是,直接写到下一级缓存或者内存的数据,很有可能并不是完整的缓存发突发写,应为store buffer也是在不断和write buffer交互的,而write buffer还要同时接受eviction buffer的请求。其结果就是写被分成几个小段。这些小块的写地址,eviction的写地址,混合着读地址,让总线和内存控制器增加了负担。它们必须采用合适的算法和参数,才能合并这些数据,更快的写到内存颗粒。\u003C\u002Fp\u003E\u003Cp\u003E然而事情还没有完。我们刚才提到,streaming模式是被触发的,同样的,它也可以退出。退出条件一般是发现存在非缓存行突发的写。这个可能受write buffer的响应时间影响。退出后,write allocate就又恢复了,从而读写地址更加不连续,内存控制器更加难以优化,延时进一步增加,反馈到处理器,就更难保持在streaming模式。\u003C\u002Fp\u003E\u003Cp\u003E再进一步,streaming模式其实存在一个问题,那就是它把数据写到了下一级缓存或者内存,万一这个数据马上就会被使用呢?那岂不是还得去抓取?针对这个问题,在ARMv8指令集中,又引入了新的一条缓存操作指令DCZVA,可以把整行缓存设成0,并且不引发write allocate。为什么?因为整行数据都被要改了,而不是某个字段被改,那就没有必要去把原来的值读出来,所以只需要allocate,不需要读取,但它还是会引发eviction。类似的,我们也可以在使用某块缓存前把它们整体清除并无效化,clean&invalidate,这样就不会有eviction。不过如果测试数据块足够大,这样只是相当于提前做了eviction,并不能消除,让写集中在某段,使之后的读更连续。\u003C\u002Fp\u003E\u003Cp\u003E以上都是针对一级缓存。二级缓存的控制力度就小些,代码上无法影响,只能通过设置寄存器,打开二级缓存预取或者设置预取偏移。我在ARM的二级缓存控制器PL301上看到的,如果偏移设置的好,抓到的数据正好被用上,可以在代码和一级缓存优化完成的基础上,读带宽再提升150%。在新的处理器上,同时可以有多路的预取,探测多组访存模板,进一步提高效率。并且,每一级缓存后面挂的OT数目肯定大于上一级,它包含了各类读写和缓存操作,利用好这些OT,就能提高性能。\u003C\u002Fp\u003E\u003Cp\u003E对于Non-Cacheable的写,它会被store buffer直接送到write buffer进行合并,然后到下一级缓存。对于Non-Cacheable的读,我们说过它会先到缓存看看是不是命中,未命中的话直接到read buffer,合并后发往下一级缓存。它通常不占用linefill buffer,因为它通常是4到8字节,不需要使用缓存行大小的缓冲。\u003C\u002Fp\u003E\u003Cp\u003E我们有时候也可以利用Non-Cacheable的读通道,和Cacheable的读操作并行,提高效率。它的原理就是同时利用linefill buffer和read buffer。此时必须保证处理器有足够的OT,不停顿。\u003C\u002Fp\u003E\u003Cp\u003E总而言之,访存的软件优化的原则就是,保持对齐,找出更多可利用的OT,访存和预取混用,保持更连续的访问地址,缩短每一环节的延迟。\u003C\u002Fp\u003E\u003Cp\u003E最后解释一下缓存延迟的产生原因。程序员可能不知道的是,不同大小的缓存,他们能达到的时钟频率是不一样的。ARM的一级缓存,16纳米工艺下,大小在32-64K字节,可以跑在2Ghz左右,和处理器同频。处理器频率再快,那么访问缓存就需要2-3个处理器周期了。但由于访问一级缓存的时间一般不会超过3个始终周期,每增加一个周期,性能就会有明显的下降。而二级缓存更慢,256K字节的,能有800Mhz就很好了。这是由于缓存越大,需要查找的目录index越大,扇出fanout和电容越大,自然就越慢。但由于访问二级缓存本身的延迟就有10个时钟周期左右,多一个周期影响没有那么明显。还有,通常处理器宣传时候所说的访问缓存延迟,存在一个前提,就是使用虚拟地址索引VIPT。这样就不需要查找一级tlb表,直接得到索引地址。如果使用物理地址索引PIPT,在查找一级tlb进行虚实转换时,需要额外时间不说,如果产生未命中,那就要到二级甚至软件页表去找。那显然太慢了。那为什么不全使用VIPT呢?因为VIPT会产生一个问题,多个虚地址会映射到一个实地址,从而使得缓存多个表项对应一个实地址。存在写操作时,多条表项就会引起一致性错误。而指令缓存通常由于是只读的,不存在这个问题。所以指令缓存大多使用VIPT。随着处理器频率越来越高,数据缓存也只能使用VIPT。为了解决前面提到的问题,ARM在新的处理器里面加了额外的逻辑来检测重复的表项。\u003C\u002Fp\u003E\u003Cp\u003E下图是真正系统里的访存延迟:\u003C\u002Fp\u003E\u003Cimg src=\&v2-cfd6c6c7ff07.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&616\& data-rawheight=\&474\&\u003E\u003Cp\u003E上图的配置中,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个处理器时钟周期。\u003C\u002Fp\u003E\u003Cp\u003E所以,即使是在3.2Gbps的DDR4上,大部分时间还都是在内存,显然优化可以从它上面入手。在处理器中的时间只有一小部分。但从另外一个方面,处理器控制着linefill,eviction的次数,地址的连续性,以及预取的效率,虽然它自己所占时间最少,但也是优化的重点。\u003C\u002Fp\u003E\u003Cp\u003E在ARM的路线图上,还出现了一项并不算新的技术,称作stashing。它来自于网络处理器,原理是外设控制器(PCIe,网卡)向处理器发送请求,把某个数据放到缓存,过程和监听snooping很类似。在某些领域,这项技术能够引起质的变化。举个例子,intel至强处理器,配合它的网络转发库DPDK,可以做到平均80个处理器周期接受从PCIe网卡来的包,解析包头后送还回去。80周期是个什么概念?看过了上面的访存延迟图后你应该有所了解,处理器访问下内存都需要200-300周期。而这个数据从PCIe口DMA到内存,然后处理器抓取它进行处理后,又经过DMA从PCIe口出去,整个过程肯定大于访存时间。80周期的平均时间说明它肯定被提前送到了缓存。 但传进来的数据很多,只有PCIe或者网卡控制器才知道哪个是包头,才能精确的推送数据,不然缓存会被无用的数据淹没。这个过程做好了,可以让软件处理以太网或者存储单元的速度超过硬件加速器。事实上,在Freescale的网络处理器上,有了硬件加速器的帮助,处理包的平均延迟还是需要200处理器周期,已经慢于至强了。其原因是访问硬件加速器本身需要设置4-8次的寄存器,而访问一次寄存器的延迟是几十纳秒,反而成为了瓶颈。\u003C\u002Fp\u003E\u003Cp\u003E如果上面一段看完你没什么感觉,那我可以换个说法:对于没有完整支持stashing的ARM SoC,哪怕处理器跑在10Ghz,网络加速器性能强的翻天,基于DPDK的简单包转发(快于Linux内核网络协议栈转发几十倍)还是只能到至强的30%,而包转发是网络处理器的最重要的指标之一,也是服务器跑网络转发软件的指标之一,更可以用在存储领域,加速SPDK之类的存储应用。\u003C\u002Fp\u003E\u003Cp\u003E还有,在ARM新的面向网络和服务器的核心上,会出现一核两线程的设计。处理包的任务天然适合多线程,而一核两线程可以更有效的利用硬件资源,再加上stashing,如虎添翼。\u003C\u002Fp\u003E\u003Cp\u003E弄清了访存的路径,可能就会想到一个问题:处理器发出去的读写请求到底是个什么东西?要想搞清楚它,就需要引入总线。下文我拿ARM的AXI\u002FACE总线协议以及由它衍生的总线结构来展开讨论。这两个协议广泛用于主流的手机芯片上,是第四代AMBA(Advanced Microcontroller Bus Architecture)标准。\u003C\u002Fp\u003E\u003Cp\u003E简单的总线就是一些地址线和数据线,再加一个仲裁器,就可以把处理器发过来的读写请求送到内存或者外设,再返回数据。在这个过程中,我们需要一个主设备,一个从设备,所有的传输都是主设备发起,从设备回应。让我们把处理器和它包含的缓存看作一个主设备,把内存控制器看作从设备。处理器发起访问请求,如果是读,那么总线把这个请求(包括地址)送到内存控制器,然后等待回应。过了一段时间,内存控制器把内存颗粒里面读出的数据交给总线,总线又把数据交给处理器。如果数据无误(ECC或者奇偶校验不出错),那么这个读操作就完成了。如果是写,处理器把写请求(包括地址)和数据交给总线,总线传递给内存控制器,内存控制器写完后,给出一个确认。这个确认经由总线又回到了处理器,写操作完成。\u003C\u002Fp\u003E\u003Cp\u003E以上过程有几个重点。第一,处理器中的单个读指令,被分为了请求(地址),完成(数据)阶段。写指令也被分为了请求(地址,数据),完成(写入确认)阶段。第二,作为从设备,内存控制器永远都无法主动发起读写操作。如果一定要和处理器通讯,比如发生了读写错误,那就得使用中断,然后让处理器来发起读写内存控制器状态的请求。第三,未完成的读写指令就变成了OT,总线可以支持多个OT。然而,总线支持多OT并不表示处理器能发送这么多请求出来,尤其是读。所以瓶颈可能还是在处理器。\u003C\u002Fp\u003E\u003Cp\u003E我遇到过几次这样的情况,在跑某个驱动的时候,突然系统挂死。但是别的设备中断还能响应,或者报个异常后系统又继续跑了。如果我们把上文的内存控制器替换成设备控制器,那就不难理解这个现象了。假设处理器对设备发起读请求,而设备没有回应,那处理器就会停在那等待。我看到的处理器,包括PowerPC, ARM,都没有针对这类情况的超时机制。如果没有中断,那处理器无法自己切换到别的线程(Linux等操作系统的独占模式),就会一直等待下去,系统看上去就挂住了。有些设备控制器可以自动探测这类超时,并通过中断调用相应的异常或者中断处理。在中断处理程序中,可以报个错,修改返回地址,跳过刚才的指令往下走,系统就恢复了。也有些处理器在触发某类异常后能自动跳到下一行指令,避免挂死。但是如果没有异常或者中断发生,那就永远挂在那。\u003C\u002Fp\u003E\u003Cp\u003E继续回到总线。在AXI\u002FACE总线协议中,读和写是分开的通道,因为他们之间并没有必然联系。更细一些,总线上规定了五个组,分别是读操作地址(主到从),读操作数据(从到主),写操作地址(主到从),写操作数据(主到从),写操作确认(从到主)。读和写两大类操作之间,并没有规定先后次序。而在每一类操作之内的组之间,是有先后次序的,比如地址是最先发出的,数据随后,可以有很多拍,形成突发操作。而确认是在写操作中,从设备收到数据之后给出的。对内存控制器,必须在数据最终写入到颗粒之后再给确认,而不是收到数据放到内部缓存中的时候。当然,这一点可以有例外,那就是提前应答early\u003Cbr\u003Eresponse。中间设备为了提高效,维护自己的一块缓冲,在收到数据后,直接向传递数据的主设备确认写入,使得上层设备释放资源。但是这样一来,由于数据并没有真正写入最终从设备,发出提前应答的中间设备必须自己维护好数据的一致性和完整性,稍不小心就会造成死锁。ARM的现有总线都不支持这个操作,都是不会告知主设备early response的,所有的内部缓冲,其实是一个FIFO,不对访问次序和应答做任何改动。\u003C\u002Fp\u003E\u003Cimg src=\&v2-4495daebe695e6a14066.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&950\& data-rawheight=\&435\&\u003E\u003Cp\u003E对于同一个通道,如果收到连续的指令,他们之间的次序是怎么样的呢?AXI\u002FACE协议规定,次序可以打乱。拿读来举例,前后两条读指令的数据返回是可以乱序的。这里包含了一个问题,总线怎么区分住前后两读条指令?很简单,在地址和数据组里加几根信号,作为标志符,来区分0-N号读请求和完成。每一对请求和完成使用相同的标志符。有了这个标志符,就不必等前一个请求完成后才开始第二个请求,而是让他们交替进行,这样就可以实现总线的OT,极大提高效率。当然,也需要提供相应的缓冲来存储这些请求的状态。并且最大的OT数取决于缓冲数和标志符中小的那个。原因很简单,万一缓冲或者标志符用完了,但是所有的读操作全都是请求,没有一个能完成怎么办?那只好让新的请求等着了。于是就有了AXI\u002FACE总线的一条规则,同一个读或者写通道中,相同标志符的请求必须按顺序完成。\u003C\u002Fp\u003E\u003Cp\u003E有时候,处理器也会拿这个标志符作为它内部的读写请求标志符,比如Cortex-A7就是这么干的。这样并不好,因为这就等于给自己加了限制,最大发出的OT不得大于总线的每通道标志符数。当一个处理器组里有四个核的时候,很可能就不够用了,人为限制了OT数。\u003C\u002Fp\u003E\u003Cp\u003E最后,读写通道之间是没有规定次序的,哪怕标志相同。\u003C\u002Fp\u003E\u003Cp\u003E看到这里可能会产生一个问题,读写指令里面有一个默认原则,就是相同地址,或者地址有重叠的时候,访存必须是顺序的。还有,如果访问的内存类型是设备,那么必须保证访存次序和指令一致。这个怎么在总线上体现出来呢?总线会检查地址来保证次序,一般是内存访问前后乱序地址不能64字节内,设备访问前后乱序地址不能在4KB内。\u003C\u002Fp\u003E\u003Cp\u003E在AXI\u002FACE中,读和写通道的比例是一比一。实际上,在日常程序中,读的概率比写要大。当然,写缓存实际上伴随着缓存行填充linefill(读),而读缓存会造成缓存行移除eviction(写),再加上合并和次序调整,所以并不一定就是读写指令的比例。我看到Freescale\u003Cbr\u003EPowerPC的总线CCB,读写通道的比率是二比一。我不知道为什么ARM并没有做类似的设计来提高效率,也许一比一也是基于手机典型应用统计所得出的最好比例。\u003C\u002Fp\u003E\u003Cp\u003E至此,我们已经能够在脑海中想象一对读写通道中读写操作的传输情况了。那多个主从设备组合起来是怎么样的情况?是不是简单的叠加?这涉及到了总线设计最核心的问题,拓扑结构。\u003C\u002Fp\u003E\u003Cp\u003E在ARM当前所有的总线产品里,根据拓扑的不同可以分为三类产品:NIC\u002FCCI系列是交叉矩阵的(Crossbar),CCN\u002FCMN系列是基于环状和网状总线的(Ring\u002FMesh),NoC系列是包转发总线(Router)。他们各有特点,适合不同场景。交叉矩阵连接的主从设备数量受到限制,但是效率最高,读写请求可以在1到2个周期内就直达从设备。如下图所示,这就是一个5x4的交叉矩阵:\u003C\u002Fp\u003E\u003Cimg src=\&v2-addeffccc172e6e4d1b88.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&1486\& data-rawheight=\&507\&\u003E\u003Cp\u003E根据我看到的数据,在28纳米制程上,5x4的配置下,这个总线的频率可以跑到300Mhz。如果进一步增加主从对数量,那么由于扇出增加,电容和走线增加,就必须通过插入更多的寄存器来增加频率。但这样一来,从主到从的延迟就会相应增加。哪怕就是保持5x3的配置,要想进一步提高到500Mhz,要么使用更好的工艺,16纳米我看到的是800Mhz;要么插入2-3级寄存器,这样,读写延时就会达到4-5个总线时钟周期,请求加完成来回总共需要10个。如果总线和处理器的倍频比率为1:2,那么仅仅是在总线上花费的时间,就需要至少20个处理器时钟周期。倍率为4,时间更长,40个时钟周期。要知道处理器访问二级缓存的延迟通常也不过10多个处理器周期。当然,可以通过增加OT数量减少平均延迟,可是由于处理器的OT数是有限制的,对于顺序处理器,可能也就是1-2个。所以,要达到更高的频率,支持更多的主从设备,就需要引入环状总线CCN系列,如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-68d3cf48a334c63cd89db6.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&690\& data-rawheight=\&423\&\u003E\u003Cp\u003ECCN总线上的每一个节点,除了可以和相邻的两个节点通讯之外,还可以附加两个节点组件,比如处理器组,三级缓存,内存控制器等。在节点内部,还是交叉的,而在节点之间,是环状的。这样使得总线频率在某种程度上摆脱了连接设备数量的限制(当然,还是受布线等因素的影响),在16纳米下,可以达到1.2GHz以上。当然,代价就是节点间通讯更大的平均延迟。为了减少平均延迟,可以把经常互相访问的节点放在靠近的位置。\u003C\u002Fp\u003E\u003Cp\u003E在有些系统里,要求连接更多的设备,并且,频率要求更高。此时环状总线也不够用了,这时需要网状总线CMN。ARM的网状总线,符合AMBA5.0的CHI接口,支持原子操作(直接在缓存运算,不用读取到处理器),stashing和直接访问(跳过中间的缓存,缩短路径)等特性,适用于服务器或者网络处理器。\u003C\u002Fp\u003E\u003Cp\u003E但是有时候,系统需要连接的设备数据宽度,协议,电源,电压,频率,都不一样,这时就需要NoC出马了,如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-75fd46e1d79bb6e750e7336.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&906\& data-rawheight=\&524\&\u003E\u003Cp\u003E这个图中,刚才提到的交叉矩阵,可以作为整个网络的某部分。而连接整个系统的,是位于NoC内的节点。每个节点都是一个小型路由,它们之间传输的,是异步的包。这样,就不必维持路由和路由之间很大数量的连线,从而提高频率,也能支持更多的设备。当然,坏处就是更长的延迟。根据我看到的数据,在16纳米上,频率可以跑到1.5Ghz。并且它所连接每个子模块之间,频率和拓扑结构可以是不同的。可以把需要紧密联系的设备,比如CPU簇,GPU放在一个子网下减少通讯延迟。\u003C\u002Fp\u003E\u003Cp\u003E在实际的ARM生态系统中,以上三种拓扑结构的使用情况是怎么样的呢?一般手机芯片上使用交叉矩阵,网络处理器和服务器上使用环状和网状拓扑,而NoC也被大量应用于手机芯片。最后一个的原因倒不是手机上需要连接的设备数太多,而是因为ARM的AXI总线NIC400对于交叉访问(interleaving)支持的非常有限。在手机里面,GPU和显示控制器对内存带宽要求是很高的。一个1080p的屏幕,每秒要刷新60次,2百万个像素,每个像素32比特颜色,再加上8层图层,就需要4GB\u002Fs的数据,双向就是8GB\u002Fs。而一个1.6GHz传输率的LPDDR4控制器,64位数据,也只能提供12.8GB\u002Fs的的理论带宽。理论带宽和实际带宽由于各种因素的影响,会有很大差别,复杂场景下能做到70%的利用率就不错了,那也就是9GB\u002Fs。那处理器怎么办?其他各类控制器怎么办?只能增加内存控制器的数量。但是,不能简单的增加数量。成本和功耗是一个原因,并且如果仅仅把不同的物理地址请求发送到不同的内存控制器上,很可能在某段时间内,所有的物理地址全都是对应于其中某一个,还是不能满足带宽要求。解决方法就是,对于任何地址,尽量平均的送到不同的内存控制器。并且这件事最好不是处理器来干,因为只有总线清楚有多少个内存控制器。最好处理器只管发请求,总线把所有请求平均分布。\u003C\u002Fp\u003E\u003Cp\u003E有时候,传输块大于256字节,可以采用一个方法,把很长的传输拆开(Splitting),分送到不同的内存控制器。不幸的是,AXI总线天然就不支持一对多的访问。原因很简单,会产生死锁。想象一下,有两个主设备,两个从设备,通过交叉矩阵连接。M1发送两个读请求,标志符都是1,先后送到到S1和S2,并等待完成。然后M2也做同样的事情,标志符都是2,先后送到S2和S1。此时,假设S2发现它如果把返回的数据次序交换一下,会更有效率,于是它就这么做了。但是M1却不能接收S2的返回数据,因为根据同标志符必须顺序完成的原则,它必须先等S1的返回数据。而S1此时也没法送数据给M2,因为M2也在等待S2返回的数据,死锁就出现了。解决方法是,AXI的Master不要发出相同标志的操作。如果标志相同时,则必须等待上一次操作完成。或者,拆分和设置新标识符操作都由总线来维护,而主设备不关心,只管往外发。\u003C\u002Fp\u003E\u003Cp\u003E在实际情况下,拆分主要用于显示,视频和DMA。ARM的CPU和GPU永远不会发出大于64字节的传输,不需要拆分。\u003C\u002Fp\u003E\u003Cp\u003E现在的中低端手机很多都是8核,而根据ARM的设计,每个处理器组中最多有四个核。这就需要放两个处理器组在系统中,而他们之间的通讯,包括大小核的实现,就需要用到总线一致性。每个处理器组内部也需要一致性,原理和外部相同,我就不单独解释了。使用软件实可以现一致性,但是那样需要手动的把缓存内容刷到下一级缓存或者内存,对于一个64字节缓存行的64KB缓存来说,需要1000次刷新,每次就算是100纳秒,且OT=4的话,也需要25微秒。对处理器来说这是一个非常长的时间。ARM使用了一个协处理器来做这个事情,这是一个解决方案。为了用硬件解决,ARM引入了几个支持硬件一致性的总线,下图是第一代方案CCI400:\u003C\u002Fp\u003E\u003Cimg src=\&v2-7d4e70f28a8db469f6cabf4.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&366\& data-rawheight=\&219\&\u003E\u003Cp\u003ECCI400是怎么做到硬件一致性的呢?简单来说,就是处理器组C1,发一个包含地址信息的特殊读写的命令到总线,然后总线把这个命令转给另一个处理器组C2。C2收到请求后,根据地址逐步查找二级和一级缓存,如果发现自己也有,那么就返回数据或者做相应的缓存一致性操作,这个过程称作snooping(监听)。具体的操作我不展开,ARM使用MOESI一致性协议,里面都有定义。在这个过程中,被请求的C2中的处理器核心并不参与这个过程,所有的工作由缓存和总线接口单元BIU等部件来做。为了符合从设备不主动发起请求的定义,需要两组主从设备,每个处理器组占一个主和一个从。这样就可以使得两组处理器互相保持一致性。而有些设备如DMA控制器,它本身不包含缓存,也不需要被别人监听,所以它只包含从设备,如上图桔黄色的部分。在ARM的定义中,具有双向功能的接口被称作ACE,只能监听别人的称作ACE-Lite。它们除了具有AXI的读写通道外,还多了个监听通道,如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-02e234b2894ffd34ff2fffa5.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&955\& data-rawheight=\&482\&\u003E\u003Cp\u003E多出来的监听通道,同样也有地址(从到主),回应(主到从)和数据(主到从)。每组信号内都包含和AXI一样的标志符,用来支持多OT。如果在主设备找到数据(称为命中),那么数据通道会被使用,如果没有,那告知从设备未命中就可以了,不需要传数据。由此,对于上文的DMA控制器,它永远不可能传数据给别人,所以不需要数据组,这也就是ACE和ACE-Lite的主要区别。\u003C\u002Fp\u003E\u003Cp\u003E我们还可以看到,在读通道上有个额外的线RACK,它的用途是,当从设备发送读操作中的数据给主,它并不知道何时主能收到这个数据,因为我们说过插入寄存器会导致总线延迟变长。万一这个时候,对同样的地址A,它需要发送新的监听请求给主,就会产生一个问题:主是不是已经收到前面发出的地址A的数据了呢?如果没收到,那它可能会告知监听未命中。但实际上地址A的数据已经发给主了,它该返回命中。加了这个RACK后,从设备在收到主给的确认RACK之前,不会发送新的监听请求给主,从而避免了上述问题。写通道上的WACK同样如此。\u003C\u002Fp\u003E\u003Cp\u003E我们之前计算过NIC400上的延迟,有了CCI400的硬件同步,是不是访问更快了呢?首先,硬件一致性的设计目的不是为了更快,而是软件更简单。而实际上,它也未必就快。因为给定一个地址,我们并不知道它是不是在另一组处理器的缓存内,所以无论如何都需要额外的监听动作。当未命中的时候,这个监听动作就是多余的,因为我们还是得从内存去抓数据。这个多余的动作就意味着额外的延迟,10加10一共20个总线周期,增长了100%。当然,如果命中,虽然总线总共上也同样需要10周期,可是从缓存拿数据比从内存拿快些,所以此时是有好处的。综合起来看,当命中大于一定比例,总体还是受益的。\u003C\u002Fp\u003E\u003Cp\u003E可从实际的应用程序情况来看,除了特殊设计的程序,通常命中不会大于10%。所以我们必须想一些办法来提高性能。一个办法就是,无论结果是命中还是未命中,都让总线先去内存抓数据。等到数据抓回来,我们也已经知道监听的结果,再决定把哪边的数据送回去。这个办法的缺点,功耗增大,因为无论如何都要去读内存。第二,在内存访问本身就很频繁的时候,这么做会降低总体性能。\u003C\u002Fp\u003E\u003Cp\u003E另外一个方法就是,如果预先知道数据不在别的处理器组缓存,那就可以让发出读写请求的主设备,特别注明不需要监听,总线就不会去做这个动作。这个方法的缺点就是需要软件干预,虽然代价并不大,分配操作系统页面的时候设下寄存器就可以,可是对程序员的要求就高了,必须充分理解目标系统。\u003C\u002Fp\u003E\u003Cp\u003ECCI总线还使用了一个新的方法来提高性能,那就是在总线里加入一个监听过滤器(Snoop\u003Cbr\u003EFilter)。这其实也是一块缓存(TAG RAM),把它所有处理器组内部一级二级缓存的状态信息都放在里面。数据缓存(DATA RAM)是不需要的,因为它只负责查看命中与否。这样做的好处就是,监听请求不必发到各组处理器,在总线内部就可以完成,省了将近10个总线周期,功耗也优于访问内存。它的代价是增加了一点缓存(一二级缓存10%左右的容量)。并且,如果监听过滤器里的某行缓存被替换(比如写监听命中,需要无效化(Invalidate)缓存行,MOESI协议定义),同样的操作必须在对应处理器组的一二级缓存也做一遍,以保持一致性。这个过程被称作反向无效化,它添加了额外的负担,因为在更新一二级缓存的时候,监听过滤器本身也需要追踪更新的状态,否则就无法保证一致性。幸好,在实际测试中发现,这样的操作并不频繁,一般不超过5%的可能性。当然,有些测试代码会频繁的触发这个操作,此时监听过滤器的缺点就显出来了。\u003C\u002Fp\u003E\u003Cp\u003E以上的想法在CCI500中实现,示意图如下:\u003C\u002Fp\u003E\u003Cimg src=\&v2-43fcfd02a35a7d310612.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&353\& data-rawheight=\&228\&\u003E\u003Cp\u003E在经过实际性能测试后,CCI设计人员发现总线瓶颈移到了访问这个监听过滤器的窗口,这个瓶颈其实掩盖了上文的反向无效化问题,它总是先于反向无效化被发现。把这个窗口加大后,又在做测试时发现,如果每个主从接口都拼命灌数据(主从设备都是OT无限大,并且一主多从有前后交叉),在主从设备接口处经常出现等待的情况,也就是说,明明数据已经准备好了,设备却来不及接收。于是,又增加了一些缓冲来存放这些数据。其代价是稍大的面积和功耗。请注意,这个缓冲和存放OT的状态缓冲并不重复。\u003C\u002Fp\u003E\u003Cp\u003E根据实测数据,在做完所有改进后,新的总线带宽性能同频增加50%以上。而频率可以从500Mhz提高到1GMhz。当然这个结果只是一个模糊的统计,如果我们考虑处理器和内存控制器OT数量有限,被监听数据的百分比有不同,命中率有变化,监听过滤器大小有变化,那肯定会得到不同的结果。\u003C\u002Fp\u003E\u003Cp\u003E作为一个手机芯片领域的总线,需要支持传输的多优先级也就是QoS。因为显示控制器等设备对实时性要求高,而处理器组的请求也很重要。支持QoS本身没什么困难,只需要把各类请求放在一个缓冲,根据优先级传送即可。但是在实际测试中,发现如果各个设备的请求太多太频繁,缓冲很快就被填满,从而阻塞了新的高优先级请求。为了解决这个问题,又把缓冲按优先级分组,每一组只接受同等或更高优先级的请求,这样就避免了阻塞。\u003C\u002Fp\u003E\u003Cp\u003E此外,为了支持多时钟和电源域,使得每一组处理器都可以动态调节电压和时钟频率,CCI系列总线还可以搭配异步桥ADB(Asynchronous Domain Bridge)。它对于性能有一定的影响,在倍频是2的时候,信号穿过它需要一个额外的总线时钟周期。如果是3,那更大些。在对于访问延迟有严格要求的系统里面,这个时间不可忽略。如果不需要额外的电源域,我们可以不用它,省一点延迟。NIC\u002FCCI\u002FCCN\u002FNoC总线天然就支持异步传输。\u003C\u002Fp\u003E\u003Cp\u003E和一致性相关的是访存次序和锁,有些程序员把它们搞混了。假设我们有两个核C0和C1。当C0和C1分别访问同一地址A0,无论何时,都要保证看到的数据一致,这是一致性。然后在C0里面,它需要保证先后访问地址A0和A1,这称作访问次序,此时不需要锁,只需要壁垒指令。如果C0和C1上同时运行两个线程,当C0和C1分别访问同一地址A0,并且需要保证C0和C1按照先后次序访问A0,这就需要锁。所以,单单壁垒指令只能保证单核单线程的次序,多核多线程的次序需要锁。而一致性保证了在做锁操作时,同一变量在缓存或者内存的不同拷贝,都是一致的。\u003C\u002Fp\u003E\u003Cp\u003EARM的壁垒指令分为强壁垒DSB和弱壁垒DMB。我们知道读写指令会被分成请求和完成两部分,强壁垒要求上一条读写指令完成后才能开始下一个请求,弱壁垒则只要求上一条读写指令发出请求后就可以继续下一条读写指令的请求,且只能保证,它之后的读写指令完成时,它之前的读写指令肯定已经完成了。显然,后一种情况性能更高,OT&1。但测试表明,多个处理器组的情况下,壁垒指令如果传输到总线,只能另整体系统性能降低,因此在新的ARM总线中是不支持壁垒的,必须在芯片设计阶段,通过配置选项告诉处理器自己处理壁垒指令,不要送到总线。但这并不影响程序中的壁垒指令,处理器会在总线之前把它过滤掉。\u003C\u002Fp\u003E\u003Cp\u003E具体到CCI总线上,壁垒机制是怎么实现的呢?首先,壁垒和读写一样,也是使用读写通道的,只不过它地址总是0,且没有数据。标志符也是有的,此外还有额外的2根线BAR0\u002F1,表明本次传输是不是壁垒,是哪种壁垒。他是怎么传输的呢?先看弱壁垒,如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-b0dca6be997.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&615\& data-rawheight=\&457\&\u003E\u003Cp\u003EMaster0写了一个数据data,然后又发了弱壁垒请求。CCI和主设备接口的地方,一旦收到壁垒请求,立刻做两件事,第一,给Master0发送壁垒响应;第二,把壁垒请求发到和从设备Slave0\u002F1的接口。Slave1接口很快给了壁垒响应,因为它那里没有任何未完成传输。而Slave0接口不能给壁垒响应,因为data还没发到从设备,在这条路径上的壁垒请求必须等待,并且不能和data的写请求交换次序。这并不能阻挠Master0发出第二个数据,因为它已经收到它的所有下级(Master0接口)的壁垒回应,所以它又写出了flag。如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-dce02baac9d7ba.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&634\& data-rawheight=\&450\&\u003E\u003Cp\u003E此时,flag在Master0接口中等待它的所有下一级接口的壁垒响应。而data达到了Slave0后,壁垒响应走到了Master0接口,flag继续往下走。此时,我们不必担心data没有到slave0,因为那之前,来自Slave0接口的壁垒响应不会被送到Master0接口。这样,就做到了弱壁垒的次序保证,并且在壁垒指令完成前,flag的请求就可以被送出来。\u003C\u002Fp\u003E\u003Cp\u003E对于强壁垒指令来说,仅仅有一个区别,就是Master0接口在收到所有下一级接口的壁垒响应前,它不会发送自身的壁垒响应给Master0。这就造成flag发不出来,直到壁垒指令完成。如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-24a61ba2f6654dda93c4a5.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&638\& data-rawheight=\&478\&\u003E\u003Cp\u003E这样,就保证了强壁垒完成后,下一条读写指令才能发出请求。此时,强壁垒前的读写指令肯定是完成了的。\u003C\u002Fp\u003E\u003Cp\u003E另外需要特别注意的是,ARM的弱壁垒只是针对显式数据访问的次序。什么叫显式数据访问?读写指令,缓存,TLB操作都算。相对的,什么是隐式数据访问?在处理器那一节,我们提到,处理器会有推测执行,预先执行读写指令;缓存也有硬件预取机制,根据之前数据访问的规律,自动抓取可能用到的缓存行。这些都不包含在当前指令中,弱壁垒对他们无能为力。因此,切记,弱壁垒只能保证你给出的指令次序,并不能保证在它们之间没有别的模块去访问内存,哪怕这个模块来自于同一个核。\u003C\u002Fp\u003E\u003Cp\u003E简单来说,如果只需要保证读写次序,用弱壁垒;如果需要某个读写指令完成才能做别的事情,用强壁垒。以上都是针对普通内存类型。当我们把类型设成设备时,自动保证强壁垒。\u003C\u002Fp\u003E\u003Cp\u003E我们提到,壁垒只是针对单核。在多核多线程时,哪怕使用了壁垒指令,也没法保证读写的原子性。解决办法有两个,一个是软件锁,一个是原子操作。AXI\u002FACE协议不支持原子操作。所以手机通常需要用到软件锁。\u003C\u002Fp\u003E\u003Cp\u003E软件锁中有个自旋锁,能用一个ARM硬件机制exclusive access来实现。当使用特殊指令对一个地址写入值,相应缓存行上会做一个特殊标记,表示还没有别的核去写这行缓存。然后下条指令读这个行,如果标记没变,说明写和读之间没有人打扰,那么就拿到锁了。如果变了,那么回到写的过程重新获取锁。由于缓存一致性,这个锁变量可以被多个核与线程使用。当然,过程中还是需要壁垒指令来保证次序。\u003C\u002Fp\u003E\u003Cp\u003E在支持ARMv8.2和AMBA 5.0 CHI接口的系统中,原子操作被重新引入。在硬件层面,其实原子操作非常容易理解,如果某个数据存在于自己的缓存,那就直接修改;如果存在于别人的缓存,那对所有其他缓存执行Eviction操作,踢出后,放到自己的缓存继续操作。这个过程其实和exclusive access非常类似。\u003C\u002Fp\u003E\u003Cp\u003E对于普通内存,还会产生一个问题,就是读写操作可能会经过缓存,你不知道数据是否最终写到了内存中。通常我们使用clean操作来刷缓存。但是刷缓存本身是个模糊的概念,缓存存在多级,有些在处理器内,有些在总线之后,到底刷到哪里算是终结呢?还有,为了保证一致性,刷的时候是不是需要通知别的处理器和缓存?为了把这些问题规范化,ARM引入了Point of Unification\u002FCoherency,Inner\u002FOuter Cacheable和System\u002FInner\u002FOuter\u002FNon Shareable的概念。\u003C\u002Fp\u003E\u003Cimg src=\&v2-1de7a5edc94baba822e6.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&937\& data-rawheight=\&439\&\u003E\u003Cp\u003EPoU是指,对于某一个核Master,附属于它的指令,数据缓存和TLB,如果在某一点上,它们能看到一致的内容,那么这个点就是PoU。如上图右侧,MasterB包含了指令,数据缓存和TLB,还有二级缓存。指令,数据缓存和TLB的数据交换都建立在二级缓存,此时二级缓存就成了PoU。而对于上图左侧的MasterA,由于没有二级缓存,指令,数据缓存和TLB的数据交换都建立在内存上,所以内存成了PoU。还有一种情况,就是指令缓存可以去监听数据缓存,此时,不需要二级缓存也能保持数据一致,那一级数据缓存就变成了PoU。\u003C\u002Fp\u003E\u003Cp\u003EPoC是指,对于系统中所有Master(注意是所有的,而不是某个核),如果存在某个点,它们的指令,数据缓存和TLB能看到同一个源,那么这个点就是PoC。如上图右侧,二级缓存此时不能作为PoC,因为MasterB在它的范围之外,直接访问内存。所以此时内存是PoC。在左图,由于只有一个Master,所以内存是PoC。\u003C\u002Fp\u003E\u003Cp\u003E再进一步,如果我们把右图的内存换成三级缓存,把内存接在三级缓存后面,那PoC就变成了三级缓存。\u003C\u002Fp\u003E\u003Cp\u003E有了这两个定义,我们就可以指定TLB和缓存操作指令到底发到哪个范围。比如在下图的系统上,有两组A15,每组四个核,组内含二级缓存。系统的PoC在内存,而A15的PoU分别在它们自己组内的二级缓存上。在某个A15上执行Clean清指令缓存,范围指定PoU。显然,所有四个A15的一级指令缓存都会被清掉。那么其他的各个Master是不是受影响?那就要用到Inner\u002FOuter\u002FNon Shareable。\u003C\u002Fp\u003E\u003Cimg src=\&v2-bc56bfda3c8f8fa705b292.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&611\& data-rawheight=\&398\&\u003E\u003Cp\u003EShareable的很容易理解,就是某个地址的可能被别人使用。我们在定义某个页属性的时候会给出。Non-Shareable就是只有自己使用。当然,定义成Non-Shareable不表示别人不可以用。某个地址A如果在核1上映射成Shareable,核2映射成Non-Shareable,并且两个核通过CCI400相连。那么核1在访问A的时候,总线会去监听核2,而核2访问A的时候,总线直接访问内存,不监听核1。显然这种做法是错误的。\u003C\u002Fp\u003E\u003Cp\u003E对于Inner和Outer Shareable,有个简单的的理解,就是认为他们都是一个东西。在最近的ARM A系列处理器上上,配置处理器RTL的时候,会选择是不是把inner的传输送到ACE口上。当存在多个处理器簇或者需要双向一致性的GPU时,就需要设成送到ACE端口。这样,内部的操作,无论inner shareable还是outer shareable,都会经由CCI广播到别的ACE口上。\u003C\u002Fp\u003E\u003Cp\u003E说了这么多概念,你可能会想这有什么用处?回到上文的Clean指令,PoU使得四个A7的指令缓存中对应的行都被清掉。由于是指令缓存操作,Inner Shareable属性使得这个操作被扩散到总线。而CCI400总线会把这个操作广播到所有可能接受的口上。ACE口首当其冲,所以四个A15也会清它们对应的指令缓存行。对于Mali和DMA控制器,他们是ACE-Lite,本不必清。但是请注意它们还连了DVM接口,专门负责收发缓存维护指令,所以它们的对应指令缓存行也会被清。不过事实上,它们没有对应的指令缓存,所以只是接受请求,并没有任何动作。\u003C\u002Fp\u003E\u003Cp\u003E要这么复杂的定义有什么用?用处是,精确定义TLB\u002F缓存维护和读写指令的范围。如果我们改变一下,总线不支持Inner\u002FOuter Shareable的广播,那么就只有A7处理器组会清缓存行。显然这么做在逻辑上不对,因为A7\u002FA15可能运行同一行代码。并且,我们之前提到过,如果把读写属性设成Non-Shareable,那么总线就不会去监听其他主,减少访问延迟,这样可以非常灵活的提高性能。\u003C\u002Fp\u003E\u003Cp\u003E再回到前面的问题,刷某行缓存的时候,怎么知道数据是否最终写到了内存中?对不起,非常抱歉,还是没法知道。你只能做到把范围设成PoC。如果PoC是三级缓存,那么最终刷到三级缓存,如果是内存,那就刷到内存。不过这在逻辑上没有错,按照定义,所有Master如果都在三级缓存统一数据的话,那就不必刷到内存了。\u003C\u002Fp\u003E\u003Cp\u003E简而言之,PoU\u002FPoC定义了指令和命令的所能抵达的缓存或内存,在到达了指定地点后,Inner\u002FOuter Shareable定义了它们被广播的范围。\u003C\u002Fp\u003E\u003Cp\u003E再来看看Inner\u002FOuter Cacheable,这个就简单了,仅仅是一个缓存的前后界定。一级缓存一定是Inner Cacheable的,而最外层的缓存,比如三级,可能是Outer Cacheable,也可能是Inner Cacheable。他们的用处在于,在定义内存页属性的时候,可以在不同层的缓存上有不同的处理策略。\u003C\u002Fp\u003E\u003Cp\u003E在ARM的处理器和总线手册中,还会出现几个PoS(Point of Serialization)。它的意思是,在总线中,所有主设备来的各类请求,都必须由控制器检查地址和类型,如果存在竞争,那就会进行串行化。这个概念和其他几个没什么关系。\u003C\u002Fp\u003E\u003Cp\u003E纵观整个总线的变化,还有一个核心问题并没有被提及,那就是动态规划re-scheduling与合并Merging。处理器和内存控制器中都有同样的模块,专门负责把所有的传输进行分类,合并,调整次序,甚至预测未来可能接收到的读写请求地址,以实现最大效率的传输。这个问题在分析性能时会重新提到。但是在总线这层,软件能起的影响很小。清楚了总线延迟和OT最大的好处是可以和性能计数器的统计结果精确匹配,看看是不是达到预期了。\u003C\u002Fp\u003E\u003Cp\u003E现在手机和平板上最常见的用法,CCI连接CPU和GPU,作为子网,网内有硬件一致性。NoC连接子网,同时连接其余的设备,包括多个内存控制器和视频,显示控制器,不需要一致性。优点是兼顾一致性,大带宽和灵活性,缺点是CPU\u002FGPU到内存控制器要跨过两个网,延迟有点大。\u003C\u002Fp\u003E\u003Cp\u003E访存路径的最后一步是内存。有的程序员认为内存是一个所有地址访问时间相等的设备,是这样的么?这要看情况。\u003C\u002Fp\u003E\u003Cp\u003EDDR地址有三个部分组成,行,bank,列。一旦这三个部分定了,那么就可以选中确定的一个物理页,通常有2-8KB大小。我们买内存的时候,有3个性能参数,比如10-10-10。这个表示访问一个地址所需要的三个操作时间,行有效(包括选bank),列选通(命令\u002F数据访问),还有预充电。前两个好理解,第三个的意思是,某个内存物理页暂时用不着,必须关闭,保持电容电压,否则再次使用这页数据就丢失了。如果连续的内存访问都是在同行同bank,那么第一和第三个10都可以省略,每一次访问只需要10单位时间;同行不同bank,表示需要打开一个新的页,只有第三个10可以省略,共20单位时间;不同行同bank,那么需要关闭老页面,打开一个新页面,预充电没法省,共30单位时间。\u003C\u002Fp\u003E\u003Cp\u003E我们得到什么结论?如果控制好物理地址,就能使某段时间内的访存都集中在一个页内,从而节省大量的时间。根据经验,在突发访问时,最多可以省50%。那怎么做到这一点?去查查芯片手册中物理内存地址到内存管脚的映射,就可以得到需要的物理地址。然后调用系统函数,为这个物理地址分配虚拟地址,就可以使得程序只访问某个固定的物理内存页。\u003C\u002Fp\u003E\u003Cp\u003E在访问有些数据结构时,特定的大小和偏移有可能会不小心触发不同行同bank这个条件。 这样可能每次访问都是最差情况。 为了避免这种最差情况的产生,有些内存控制器可以自动让最终地址哈希化,打乱原有的不同行同bank条件,从而在一定程度上减少延迟。我们也可以通过计算和调整软件物理地址来避免上述情况的发生。\u003C\u002Fp\u003E\u003Cp\u003E在实际的访问中,通常无法保证访问只在一个页中。DDR内存支持同时打开多个页,比如4个。而通过交替访问,我们可以同时利用这4个页,不必等到上一次完成就开始下一个页的访问。这样就可以减少平均延迟。如下图: \u003C\u002Fp\u003E\u003Cimg src=\&v2-e8ee6e44f97a95c569baf18c1a293353.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&766\& data-rawheight=\&393\&\u003E\u003Cp\u003E我们可以通过突发访问,让上图中的绿色数据块更长,那么相应的利用率就越高。此时甚至不需要用到四个bank,如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-149e7d75bcf05ce5dc40c960fe5f5a7c.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&760\& data-rawheight=\&236\&\u003E\u003Cp\u003E如果做的更好些,我们可以通过软件控制地址,让上图中的预充电,甚至行有效尽量减少,那么就可以达到更高的效率。还有,使用更好的内存颗粒,调整配置参数,减少行有效,列选通,还有预充电的时间,提高DDR传输频率,也是好办法,这点PC机超频玩家应该有体会。此外,在DDR板级布线的时候,控制每组时钟,控制线,数据线之间的长度差,调整好走线阻抗,做好自校准,设置合理的内存控制器参数,调好眼图,都有助于提高信号质量,从而可以使用更短的时序参数。\u003C\u002Fp\u003E\u003Cp\u003E如果列出所有数据突发长度情况,我们就得到了下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-dbd27ee5c5dcc4.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&822\& data-rawheight=\&576\&\u003E\u003Cp\u003E上面这个图包含了更直观的信息。它模拟内存控制器连续不断的向内存颗粒发起访问。X轴表示在访问某个内存物理页的时候,连续地址的大小。这里有个默认的前提,这块地址是和内存物理页对齐的。Y轴表示同时打开了多少个页。Z轴表示内存控制器访问内存颗粒时带宽的利用率。我们可以看到,有三个波峰,其中一个在128字节,利用率80%。而100%的情况下,访问长度分别为192字节和256字节。这个大小恰恰是64字节缓存行的整数倍,意味着我们可以利用三个或者四个8拍的突发访问完成。此时,我们需要至少4个页被打开。\u003C\u002Fp\u003E\u003Cp\u003E还有一个重要的信息,就是X轴和Z轴的斜率。它对应了DDR时序参数中的tFAW,限定单位时间内同时进行的页访问数量。这个数字越小,性能可能越低,但是同样的功耗就越低。\u003C\u002Fp\u003E\u003Cp\u003E对于不同的DDR,上面的模型会不断变化。而设计DDR控制器的目的,就是让利用率尽量保持在100%。要做到这点,需要不断的把收到的读写请求分类,合并,调整次序。而从软件角度,产生更多的缓存行对齐的读写,保持地址连续,尽量命中已打开页,减少行地址和bank地址切换,都是减少内存访问延迟的方法。\u003C\u002Fp\u003E\u003Cp\u003E交替访问也能提高访存性能。上文已经提到了物理页的交替,还可以有片选信号的交替访问。当有两个内存控制器的时候,控制器之间还可以交替。无论哪种交替访问,都是在前一个访问完成前,同时开始下一个传输。当然,前提必须是他们使用的硬件不冲突。物理页,片选,控制器符合这一个要求。交替访问之后,原本连续分布在一个控制器的地址被分散到几个不同的控制器。最终期望的效果如下图:\u003C\u002Fp\u003E\u003Cimg src=\&v2-76eb0f3ae88cfebc6a906b.jpg\& data-caption=\&\& data-size=\&normal\& data-rawwidth=\&565\& data-rawheight=\&368\&\u003E\u003Cp\u003E这种方法对连续的地址访问效果最好。但是实际的访存并没有上图那么理想,因为哪怕是连续的读,由于缓存中存在替换eviction和硬件预取,最终送出的连续地址序列也会插入扰动,而如果取消缓存直接访存,可能又没法利用到硬件的预取机制和额外的OT资源。实测下来,可能会提升30%左右。此外,由于多个主设备的存在,每一个主都产生不同的连续地址,使得效果进一步降低。因此,只有采用交织访问才能真正的实现均匀访问多个内存控制器。当然,此时的突发长度和粒度要匹配,不然粒度太大也没法均匀,就算均匀了也未必是最优的。对于某个内存控制来说,最好的期望是总收到同一个物理页内的请求。\u003C\u002Fp\u003E\u003Cp\u003E还有一点需要提及。如果使用了带ecc的内存,那么最好所有的访问都是ddr带宽对齐(一般64位)。因为使能ecc后,所有内存访问都是带宽对齐的,不然ecc没法算。如果你写入小于带宽的数据,内存控制器需要知道原来的数据是多少,于是就去读,然后改动其中一部分,再计算新的ecc值,再写入。这样就多了一个读的过程。根据经验,如果访存很多,关闭ecc会快8%。\u003C\u002Fp\u003E\u003Cp\u003E下面是软件层面可以使用的优化手段:\u003C\u002Fp\u003E\u003Cp\u003E面向处理器结构的优化可以从以下几个方向入手:缓存命中,指令预测,数据预取,数据对齐,内存拷贝优化,ddr访问延迟,硬件内存管理优化,指令优化,编译器优化等级以及性能描述工具。\u003C\u002Fp\u003E\u003Cp\u003E缓存未命中是处理器的主要性能瓶颈之一。在FSL的powerpc上,访问一级缓存是3个时钟周期,二级是12个,3级30多个,内存100个以上。一级缓存和内存访问速度差30多倍。我们可以算一下,如果只有一级缓存和内存,100条存取指令,100%命中和95%命中,前者300周期,后者95*3+5*100=785周期,差了1.6倍。这个结果的前提是powerpc上每个核心只有1个存取单元,使得多发射也无法让存取指令更快完成。当然,如果未命中的指令分布的好,当中穿插了很多别的非存取指令那就可以利用乱序多做些事情,提高效率。\u003C\u002Fp\u003E\u003Cp\u003E我们可以用指令预测和数据预取。\u003C\u002Fp\u003E\u003Cp\u003E指令预测很常见,处理器预测将要执行的一个分支,把后续指令取出来先执行。等真正确定判断条件的时候,如果预测对了,提交结果,如果不对,丢掉预先执行的结果,重新抓取指令。此时,结果还是正确的,但是性能会损失。指令预测是为了减少流水线空泡,不预测或者预测错需要排空流水线并重新从正确指令地址取指令,这个代价(penalty)对流水线深度越深的处理器影响越大,严重影响处理器性能。\u003C\u002Fp\u003E\u003Cp\u003E指令预测一般是有以下几种办法:分支预测器(branch predictor)+btb+ras(Return\u003Cbr\u003EAddress 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\u002FUNLIKELY修饰判断条件,也能提高准确率。需要注意的是,btb表项是会用完的,也就是说,如果程序太久没有走到上次的记录点,那么记录就会被清掉,下次再跑到这就得重新记录了。分支预测有个有趣的效应,如果一段代码处于某个永远不被触发的判断分支中,它仍然可能影响处理器的分支预测,从而影响总体性能。如果你删掉它,说不定会发现程序奇迹般的更快了。\u003C\u002Fp\u003E\u003Cp\u003E数据预取,和指令预测类似,也是处理器把可能会用到的数据先拿到缓存,之后就不必去读内存了。它又分为软件预取和硬件预取两种,硬件的是处理器自己有个算法去预测抓哪里的数据,比如在访问同一类型数据结构的某个元素,处理器会自动预取下一个偏移的数据。当然,具体算法不会这么简单。软件预取就是用编译器的预编译宏修饰某个将要用到的变量,生成相应指令,手工去内存抓某个程序员认为快要用到的数据。为什么要提前?假设抓了之后,在真正用到数据前,有100条指令,就可以先执行那些指令,同时数据取到了缓存,省了不少时间。\u003C\u002Fp\u003E\u003Cp\u003E需要注意的是,如果不是计算密集型的代码,不会跑了100个周期才有下一条存取指令。更有可能10条指令就有一次访存。如果全都未命中,那么这个预取效果就会打不少折扣。并且,同时不宜预取过多数据,因为取进来的是一个缓存行,如果取得过多,会把本来有用的局部数据替换出去。按照经验同时一般不要超过4条预取。此外,预取指令本身也要占用指令周期,过多的话,会增加每次循环执行时间。要知道有时候1%的时间都是要省的。\u003C\u002Fp\u003E\u003Cp\u003E在访问指令或者数据的时候,有一个非常重要的事项,就是对齐。四字节对齐还不够,最好是缓存行对齐,一般是在做内存拷贝,DMA或者数据结构赋值的时候用到。处理器在读取数据结构时,是以行为单位的,长度可以是32字节或更大。如果数据结构能够调整为缓存行对齐,那么就可以用最少的次数读取。在DMA的时候一般都以缓存行为单位。如果不对齐,就会多出一些传输,甚至出错。还有,在SoC系统上,对有些设备模块进行DMA时,如果不是缓存行对齐,那么可能每32字节都会被拆成2段分别做DMA,这个效率就要差了1倍了。\u003C\u002Fp\u003E\u003Cp\u003E如果使用了带ecc的内存,那么更需要ddr带宽对齐了。因为使能ecc后,所有内存访问都是带宽对齐的,不然ecc没法算。如果你写入小于带宽的数据,内存控制器需要知道原来的数据是多少,于是就去读,然后改动其中一部分,再计算新的ecc值,再写入。这样就多了一个读的过程,慢不少。\u003C\u002Fp\u003E\u003Cp\u003E还有一种需要对齐情况是数据结构赋值。假设有个32字节的数据结构,里面全是4字节元素。正常初始化清零需要32\u002F4=8次赋值。而有一些指令,可以直接把缓存行置全0或1。这样时间就变成1\u002F8了。更重要的是,写缓存未命中实际上是需要先从内存读取数据到缓存,然后再写入。这就是说写的未命中和读未命中需要一样的时间。而用了这个指令,可以让存指令不再去读内存,直接把全0\u002F1写入缓存。这在逻辑上是没问题的,因为要写入的数据(全0\u002F1)已经明确,不需要去读内存。以后如果这行被替换出去,那么数据就写回到内存。当然,这个指令的限制也很大,必须全缓存行替换,没法单个字节修

我要回帖

更多关于 arm cortex a53好不好 的文章

 

随机推荐