zoukankan      html  css  js  c++  java
  • 对于流程优化的处理器架构

    在过去的两年里,我学到了很多的代码优化方法的同事,在此汇总了什么。

    优化处理器架构可以从下面几个方向展开:高速缓存命中。指令预测。数据预取,数据对齐,内存拷贝优化,ddr访问延迟。硬件内存管理优化,指令优化。叙述工具。

    缓存未命中是处理器的主要性能瓶颈之中的一个。在FSL的powerpc上,訪问一级缓存是3个时钟周期,二级是12个,3级30多个。内存100个以上。一级缓存和内存訪问速度差30多倍。

    我们能够算一下,假设仅仅有一级缓存和内存,100条存取指令。100%命中和95%命中。前者300周期,后者95*3+5*100=785周期,差了1.6倍。这个结果的前提是powerpc上每一个核心仅仅有1个存取单元,使得多发射也无法让存取指令更快完毕。

    当然,假设未命中的指令分布的好,其中穿插了非常多别的非存取指令那就能够利用乱序多做些事情,提高效率。

    怎么用代码提高缓存命中率?我们能够用指令预測和数据预取。

    指令预測非经常见,处理器预測将要运行的一个分支,把兴许指令取出来先运行。

    等真正确定推断条件的时候,假设预測对了,提交结果。假设不正确,丢掉预先运行的结果。又一次抓取指令。

    此时,结果还是正确的,可是性能会损失。

    有一个经常使用的指令预測机制叫btb(branch target buffer),大致方法是,对于跳转指令,把它近期几次的跳转结果记录下来,作为下一次此处程序分支预測的根据。

    举个样例,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訪问优化。通常软件project师觉得内存是一个全部地址訪问时间相等的设备,是这种么?这要看情况。我们买内存的时候,有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%性能,结果发现算法本身复杂度太高了,改进下算法。可能是几倍的提升。还有,在优化之前,自己首先要对数据流要有清楚的认识,然后再用工具来印证这个认识。就像设计前端数字模块,首先要在心里有大致模型,再去用描写叙述语言实现。而不是写完代码综合下看看结果。

    TBD:补充数据结构地址造成的缓存和内存款颠簸http://lwn.net/Articles/255364/

    版权声明:本文博主原创文章,博客,未经同意不得转载。

  • 相关阅读:
    迭代器在LinkedList上的删除
    java多线程:CopyOnWriteArrayList
    vs中代码编译通过,但还是有红色波浪线
    vs中项目属性配置
    TortoiseGit安装与配置
    DC(device context)
    weak_ptr 使用
    C++ 中shared_ptr循环引用计数问题
    for_each与lambda表达式联合使用
    new 和 make_shared 在内存上的区别
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/4884234.html
Copyright © 2011-2022 走看看