打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
一秒钟的时间也要全力争取(上)

优化等级越高编程时间反而增加?

SmartPro 6000F使用全FPGA架构,并内嵌了4颗Nios II软核,我们使用的开发环境是Nios for Eclipse,编译器是GCC。在帮客户定制一款NAND Flash编程时序时,为了进一步提高编程速度,准备对时序进行优化。默认情况下,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时,这两个函数会按照如下所示顺序的加载到Cache里。 

 研究了一下Cache的数据结构后发现,数据并不是简单的顺序存储在Cache里,不同原理的Cache,使用的数据结构也不同,从Nios II开发手册里获知,当前平台使用的是直接映射结构的Cache,数据以散列的格式存储,为了简化和提高Cache的效率,Nios II 里的Cache利用了一个最简单的散列函数:

 其中cache_addr为Cache地址,sram_addr为内存SRAM地址,cache_size为Cache大小,这里为4K,所以Program和Timer函数在Cache里的实际存储格式是:

 由上可以看到,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里,将函数到分布到指定的段的语法如下:

 通过方法二的改进后,不同优化等级下Program的运行时间变成了:O0 30s, O1 25s,O2 24s,又比之前缩短了1s。

以前一直听说加入Cache后,程序的运行就会变的不可预测,这次算是彻底的感受到了,但不可预测并不代表不可控,通过上述的两种方法,就可以控制函数尽量不去访问内存,提高执行效率和运行的一致性。

好景不长,发展总是螺旋上升的

但是好景不长,在给客户增加了一个小功能后,Cache又犯毛病了,不过这次出问题的不是指令Cache,而是数据Cache,详文请期待“毫秒必争之如何搞定Cache(下)”。

了解更多关于SmartPro 6000F的信息请登陆网址:

http://www.zlg.cn/sitecn/program/cate_62.html

 

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
内核编程
cuda的dll开发流程
剖析Disruptor:为什么会这么快?(二)神奇的缓存行填充 | 并发编程网
性能优化的方法和技巧:代码
V8 Design Elements(翻译)
如何评价「线程的本质就是一个正在运行的函数」?
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服