zoukankan      html  css  js  c++  java
  • 毫秒必争之如何搞定cache(上)

    本文以SmartPro 6000F使用的nios ii内核为例,详述了如何搞定cache,将程序的运行时间从最开始的30s优化到25s,再从25s优化到最终的24s。尤其是那最后1s的优化,遇到了很多问题,而这些问题在嵌入式系统里,任何一款配置了cache的处理器都可能会碰到,所以特撰此文献给那些还在倍受cache折磨的工程师们。全文分上下两部,上部为如何搞定指令cache,下部为如何搞定数据cache。

     
    SmartPro 6000F使用全FPGA架构,并内嵌了4颗nios II软核,我们使用的开发环境是nios for eclipse,编译器是gcc。在帮客户定制一款nandflash编程时序时,为了进一步提高编程速度,准备对时序进行优化。默认情况下,gcc的优化等级为最低的O0,编程时间为30s,开启O1一级优化后,编程时间降到了25s,当开启O2二级优化后,编程时间没有继续下降,反而又上升到了29s。
     
    虽然不同优化等级之间时间差别就不到5s,我们完全可以很省事的开启O1级别优化时序,然后就交付给客户,但是当时我们并没有这么做。因为理论上,优化等级越高,编程时间应该越少才对,但是现在测试的编程时间结果是 O0 > O2 > O1,冥冥中感觉时序还可以再优化些,速度还可再快一点。
     
    目前提速遇到的问题是:使用更高的优化等级,编程时间反而更多了,这可不符合gcc的优化规律阿。那是什么的存在破坏了优化规律呢?宏观的考虑,在嵌入式系统里,能破坏程序运行规律的家伙,嫌疑最大的就是cache了。
     
    cache存在的初衷是为了提速,因为程序指令如果完全运行在内存里,速度会非常慢,而在cache里运行将非常快,但是cache的容量是有限的,无法缓存所有程序,所以nios ii内核在设计的时候做了一个折中处理,先将内存里的程序搬运到cache里,然后在cache里运行程序,由于cache无法一次性缓存所有程序,如果运行的程序大小超过了cache容量,必须要重新访问内存更新cache,如果更新频率越高,则访问内存的次数就越多,运行效率自然就会被拉低,而cache的更新频率是不可预测的,所以配置了cache的嵌入式系统的运行时间一般都很难预测。
     
    但是,SmartPro 6000F里的每一个nios ii内核都配置了4KB的指令cache和2KB的数据cache,而编程时序只有不到2KB,理论上完全可以缓存到cache里运行,根本不用去更新cache,也就不应该存在运行不规律的问题了。
     
    但是,但是,上述只是我们的一个理论分析,实际运行到底有没有更新cache,还需实际测试说了算。我们将6000F的内存sram的读(rd)、写(wr)和地址(addr)连上逻辑分析仪观察,如果rd线或者wr线有出现脉冲,就说明存在访问内存更新cache的操作。不同的优化等级下,测试波形如下所示:
     
    O1   25s
    O2   29s
    O0   30s
     
    由上图发现,运行时间较长的,都不同程度的出现了访问内存的现象,而且访问的越频繁,速度越慢。编程时序的大小已经完全可以加载到cache里运行,为什么还会访问内存呢?
     
    看来cache的加载方式好像没有之前想的那么简单,为了解决这个疑惑,让我们再来研究下在O2优化等级时编程时序的汇编代码。编程函数Program在内存sram的地址分布区域从0x1014到0x15F0,里面会调用到的一个定时器函数Timer在内存的分布从0x2020到0x2170。
     
    Program( )
    {
    0x1014
    0x1018
    0x101c    call Timer( )
    …
    0x15F0
    }
    …
    Timer( )
    {
    0x2020
    0x2024
    ...
    0x2170
    }

    之前天真的以为在运行Program时,这两个函数会按照如下所示顺序的加载到cache里。

     
    cache_addr     sram_addr
    0x0             0x1014                Program( )
    0x4             0x1018             
    0x8             0x101c
     0xc             0x2020                Timer( )
     0x10            0x2024
     0x14            0x2028
     ...
     0x5c            0x2070
     0x60            0x1020
     ...
     0x62c           0x15f0
    研究了一下cache的数据结构后发现,数据并不是简单的顺序存储在cache里,不同原理的cache,使用的数据结构也不同,从nios ii开发手册里获知,当前平台使用的是直接映射结构的cache,数据以散列的格式存储,为了简化和提高cache的效率,nios ii 里的cache利用了一个最简单的散列函数:
     
    cache_addr = sram_addr  mod  cache_size
    其中cache_addr为cache地址,sram_addr为内存sram地址,cache_size为cache大小,这里为4K,所以Program和Timer函数在cache里的实际存储格式是
     
     
    cache_addr  Program( )    Timer( )
    0x0            
    ...
    0x14          0x1014             
    0x18          0x1018
    0x1c          0x101c
    0x20          0x1020       0x2020
    0x24          0x1024       0x2024
    ...            ...
    0x70          0x1070       0x2070
    0x74          0x1074
    ...
    0x5F0         0x15f0
    由上可以看到,cache里,从0x20地址开始,Program和Timer的加载发生了冲突。编程时序运行时,cache里首先存的是Program,当运行到Timer时,nios ii会从内存调取Timer的函数存入0x20开始的cache,并覆盖Program的一部分函数,当Timer执行完后,继续运行Program,nios ii又要从内存获取Program中被覆盖的那部分程序,调入cache里执行,这样每执行一次Program函数,就会更新两次cache。
     
    到这里,所有问题似乎都豁然开朗了,不同等级的优化设置后,在改变函数大小的同时,也会改变它们在内存的地址分布。Program和Timer的分布地址,通过cache的散列后如果没有冲突,那么在运行时就不会访问内存,如果产生了冲突,并且冲突的地址越多,则访问内存的时间就会越长,整体速度就会越慢。O2的优化等级比O1高,虽然前者优化后的程序更小,但是前者在cache的散列加载地址发生了冲突,速度自然就更慢了。
     
    要解决冲突问题,必须从cache的散列函数入手。
     
    方法一:增大cache的容量
    由于Program和Timer的函数分布地址跨度过大,超过了cache_size,才导致散列后发生冲突,如果将cache_size增大到8KB,Program在cache里的加载地址是0x1014,Timer在cache里的加载地址是0x20,不会发生冲突。但是嵌入式系统里的资源都非常精贵,很多系统无法提供这么大的cache,此时可以采用另一种更实惠的方法。
     
    方法二:通过分散加载,将Program和Timer的分布地址跨度缩小到cache_size内
    在bsp里,新增一个从0x1000到0x2000的段.UserCache,然后将Program和Timer强制分布到这个段里,这样两个函数在cache里的存储地址也不会冲突了。gcc里,将函数到分布到指定的段的语法如下:
     
    int  Program( ) __attribute__ ((section(“.UserCache ")))

     

    通过方法二的改进后,不同优化等级下Program的运行时间变成了:O0 30s, O1 25s,O2 24s,又比之前缩短了1s。
     
    以前一直听说加入cache后,程序的运行就会变的不可预测,这次算是彻底的感受到了,但不可预测并不代表不可控,通过上述的两种方法,就可以控制函数尽量不去访问内存,提高执行效率和运行的一致性。
     
    但是好景不长,在给客户增加了一个小功能后,cache又犯毛病了,不过这次出问题的不是指令cache,而是数据cache,详文请看“毫秒必争之如何搞定cache(下)”。
  • 相关阅读:
    hdu1828(线段树——矩形周长并)
    hdu1255(线段树——矩形面积交)
    用jQuery获取到一个类名获取到的是一个数组 ,如果对数组中的每个进行相应的操作可以这样进行
    CSS3向外扩散的圆
    鼠标放上去图片会放大
    Django分页
    Django使用富文本编辑器
    Django日志配置
    Linux中的文件类型
    Linux压缩和解压缩
  • 原文地址:https://www.cnblogs.com/littlexiaocai/p/3078255.html
Copyright © 2011-2022 走看看