little小蔡

【嵌入式】程序为什么越优化越慢?

0
阅读(2471)

正在开发一个基于Nios II内核的项目,使用的开发环境是nios for eclipse,编译器是GCC,整体功能实现后,开始优化速度。默认没有开启gcc的优化选项,一段关键函数Key的运行时间为30s,开启O1一级优化后,程序大小从15KB减小到12KB,但运行时间增加到了35s,开启O2后,程序大小没有明显的减少,但运行时间明显提速到了23s,为了赶工期,暂时没去追究O1导致降速的原因,一直开着O2继续测试程序。


直到修改完另外一个和关键程序毫无耦合关系的函数A,回过头又去测试了下那段关键函数Key的运行时间,在O2的情况下尽然又降到了29s,改为O1后,又回到了25s,不开优化的时间还是30s。这次出现的问题和上次非常类似,归纳如下:

修改无关函数A之前,Key运行时间 O1 > O0 > O2

修改无关函数A之后,Key运行时间 O0 > O2 > O1


为什么优化等级变高后,运行时间反而更慢了呢?而且在不同的情况下,优化的效果还不一致,没有规律可寻,gcc的优化不会这么不靠谱吧。


对于一个看似毫无规律可寻的问题,有时解决之道可能就蕴藏在问题本身,先不去考虑问题的表象为什么没有规律,在一个嵌入式平台里,能让程序运行时间不可测的家伙,最大的嫌疑可能就是cache了。


cpu从主存里取指令运行的很慢,而从cache里运行的很快,所以cpu会将经常用到程序段先缓存到cache里,然后在cache里运行程序。但是cache容量有限,无法缓存所有的程序,此时就会出现访问主存更新cache的操作,如果更新的频率越频繁,访问主存的次数越多,运行的总体效率就会被拉下来,所以为了提高效率,关键函数及其需要调用的函数大小之和都要尽量控制在cache的容量范围之内。


当前的nios内核配置了4KB的程序cache和2KB的数据cache,执行的关键函数和其需要调用的函数总大小只有不到2KB,理论上都可以缓存到cache里执行,不用去访问主存。

但实际情况并非这么简单。将当前平台使用到的主存sram的rd、wr和地址线连上逻辑分析仪观察,如果rd线或者wr线有出现脉冲就说明CPU有访问主存的操作,不同的情况下测试波形如下所示:

O1   25s

O2   29s

O0   30s


由上图发现,运行时间较长的程序,都出现了访问外存的情况,而且访问的越频繁,速度越慢,关键函数和其调用的所有函数容量小到可以完全加载到cache里,为什么还会访问外存呢?让我们对cache再进行些深入的研究。


关键函数Key里会调用到B函数,编译后,两个函数在map里的分布示意图如下所示,Key在主存的分布地址从0x1014到0x15F0,B函数在主存的分布地址从0x2020到0x2170。

Key( )

{

0x1014

0x1018

0x101c    call B()

0x15F0

}

B( )

{

0x2020

0x2024

...

0x2170

}


曾今,我以为在运行Key时,这两个函数会按照如下所示顺序存储在cache里的。

cache地址   程序地址

0x0             0x1014

0x4             0x1018             

0x8             0x101c

0xc             0x2020

0x10           0x2024

0x14           0x2028

...

0x5c           0x2070

0x60           0x1020

...

0x62c          0x15f0


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


cache地址  Key( )       B( )

0x0            

...

0x14           0x1014             

0x18           0x1018

0x1c           0x101c

0x20           0x1020    0x2020

0x24           0x1024    0x2024

...                      ...

0x70           0x1070    0x2070

0x74           0x1074

...

0x5F0         0x15f0


从0x20地址开始,Key和B发生了冲突。执行时,cache里首先存的是Key,当运行到要调用B时,cpu会从主存调取B的函数存入0x20开始的cache,把原来Key的一部分函数给覆盖了,当B执行完后,继续运行Key时,cpu又要从主存获取Key中被覆盖的那部分程序,调入cache里执行,这样每执行一次Key函数,就要访问两次主存。


修改其他函数或是设置不同的优化,在改变函数大小的同时,也改变了它们在主存的分布地址,如果Key和B的分布地址,通过在cache的散列后没有冲突,那么在运行时就不会取访问主存,如果产生了冲突,两者重叠的地址越多,访问主存的时间就会越长,整体速度就会越慢,这才是越优化越慢的根本原因。


要解决它,必须要从cache的散列函数入手。


方法一:增大cache的容量

由于Key和B的函数分布地址跨度过大,超过了cache_size,才导致散列后发生冲突,如果将cache_size增大到8KB,Key存放在cache里的起始地址是0x1014,B在cache里的起始地址是0x20,冲突自然就消失了。但是嵌入式系统里的资源都非常有限,很多系统无法提供更大的cache,此时可以采用另一种更实惠的方法。


方法二:将Key和B的分布地址跨度缩小到cache_size内

在bsp里,新增一个从0x1000到0x2000的段.KeyCache,然后将Key和B强制分布到这个段里,这样两个函数在cache里的存储地址也不会冲突了。gcc里,将函数到分布到指定的段的语法如下:

int  Key( ) __attribute__ ((section(".Keycache ")))


通过方法二改进后,Key的运行时间变为:O0 30s, O1 27s,O2 23s,回到了预期的状态。


一直听说加入cache后,程序的运行就会变的不可预测,这次算是彻底的感受到了,但不可预测并不代表不可控,通过上述的两种方法,就可以控制函数尽量不去访问主存,提高执行效率和运行的一致性。但是好景不长,修改了一些其他函数后,O2情况下的Key函数执行时间又莫名其妙增到了30s,测试发现,cpu又去访问主存了,不过这次,访问的主角从指令变成了数据,而且很奇怪的是,程序中所有的全局变量只有不到1KB,而数据cache开了2KB的空间,根据cache的散列函数,数据存储是永远不会发生冲突的,至于为什么还会访问主存,放到后续的文章里再说吧。