【再话ZedBoard】ARM软件优化,从1s到10ms
0赞1、前言
感觉很久没有写博了,最近一直在搞程序的优化,利用arm cortex-a9生成一副1024*768的图像,最初的作图时间将近1s,简直高的不能再高了,如果作一幅图都需要1s,那动画显示就是一部经典大片的名字——《Mission Impossible》。工作不能不做,所以就只能想办法把1s的作图时间缩短到1/24s,约41.7ms。面对如此头疼的事,每天尽想着改进算法和程序,也就没什么心情写博客了。
2、常规优化
首先说明:如果设置了编译器优化选项,该部分的优化可能不起作用,甚至会增加程序执行时间,总的来说这部分用处不是太大,一个编译器优化选项就搞定了。如果只关心一些针对zedboard或者是zynq平台具体优化做法,可以跳过这一部分,直接看我的优化手段。
关于这部分,网上有一些经典的文章,本文给出一个链接:
http://www.cnblogs.com/goodloop/archive/2010/10/02/1841171.html
链接中的文章,提到的优化策略有很多,我简单列出几点:
i) 利用移位代替乘/除2的操作,利用移位加法代替乘法;
ii) 优化循环终止条件:使用减计数代替惯用的加计数,使用i!=0作循环终止条件;
iii) 使用合适的变量类型:处理器字长为32bits,所以使用8bits、16bits数据类型会增加处理难度;
除此之外,我想说一下ARM官方编程手册《Cortex-A Series Programmer's Guide》里提到的优化方法:
i)、使用inline函数
inline函数主要节省了调用函数所占用的时间。这里需要注意的是GCC对inline函数的处理方式:inline关键字仅在同一个编译单元内有效,也就是说,如果想要在a.c里调用b.c里定义的inline函数需要做特殊处理,关于这个本文不多做说明,详细情况请google或者百度一下。
ii)、充分利用已经得出计算结果的变量
这种说法听起来比较拗口,还是举例说明比较清晰:
---------------------代码开始------------------------
i = a * b + c;
j = a * b * d;
---------------------代码结束------------------------
可以优化为:
---------------------代码开始------------------------
tmp = a * b;
i = tmp + c;
j = tmp * d;
---------------------代码结束------------------------
iii)、避免循环
例如,需要进行小批量数据的赋值时,可以不使用循环,例如:
---------------------代码开始------------------------
for (i = 0; i < 10; i++)
{
x[i] = i;
}
---------------------代码结束------------------------
可以优化为:
---------------------代码开始------------------------
x[0] = 0;
x[1] = 1;
x[2] = 2;
x[3] = 3;
x[4] = 4;
x[5] = 5;
x[6] = 6;
x[7] = 7;
x[8] = 8;
x[9] = 9;
---------------------代码结束------------------------
iv)、结构对齐、循环优化、变量选择等
这也属于常规优化,我就不细说了,可以参考上文提供的链接,或者自行搜索。
~to be continued
3、几种对我有所帮助的最重要的优化手段
i)、对于频繁使用的函数,不要给它穿太多衣服。
还是举例说比较清晰:
起初我使用显示IP核提供的驱动程序,其中像素描绘函数drawPixel()是这样实现的:drawPixel()调用了函数B(),函数B()又调用了函数C(),最后通过函数指针对C()进行一次赋值调用,在这一层才真正实现对DDR3写操作。这种写法想必有其优点,如:可以实现对函数的动态赋值,对底层函数进行了较好的封装等。
像素描绘函数实质上就是向显存某个地址写入颜色数据,因此,我考虑自己编写像素操作函数,我的drawPixel()不需要穿那么多衣服,一件足以——直接操作DDR3的与像素坐标对应的存储地址。这样以来,减少了函数调用的开销,避免了函数指针的使用,势必将节省大量时间,事实正是如此:由于ARM程序的主要功能就是生成显示画面,所以drawPixel()的使用相当频繁,对drawPixel()作出改进之后,大大节省了作图时间,从最开始的980ms,减少到560ms。
ii)、SDK中设置编译器优化选项。
设置方法如下:
a)、菜单栏选择project菜单:
b)、选择属性选项,即Properties,弹出对话框如下:
c)、在C/C++ Build选项下,选择Setting,在右边的Tools Setting选项卡下,找到ARM gcc compiler,在该栏下选择Optimization,右边就可以看到优化等级(Optimization Level)选项,各个选项在下图中用红色圆圈标出:
d)、根据需要设置优化选项:
这里说一下,优化选项的意义:
优化命令是由连接符-加上大写英文字母O后面紧跟阿拉伯数字(0、1、2、3、time、space)组成的,刚开始我在makefile相关文件设置优化选项,由于在看资料时,我把英文字符O看成数字0了,导致了一些错误。
-Ospace:命令编译器尽可能地减小代码尺寸,可以以牺牲代码执行效率为代价;
-Otime:命令编译器尽可能地提高执行速度,可以以牺牲代码尺寸为代价;
-O0:最低等级的优化选项,关闭绝大多数优化,提供全面的调试信息;
-O1:去除无用的静态函数和内联函数;避免严重影响调试信息的优化;在代码密度和调试信息之间寻求较好的平衡;
-O2:高级优化;有些调试信息可能无法看到,经过优化后的代码和源码差异较大;
-O3:和-O2一样,但是可以设置对代码尺寸和执行时间有所侧重的优化。-O3 -Otime执行速度快于 -O2 -Otime,但代码尺寸有所增加;-O3 -Ospace代码尺寸小于 -O2 -Ospace,但执行速度可能要慢一些。
设置完编译器优化选项之后,代码执行效率又有了较大幅度地提高,从560ms减少到330ms左右。
这里需要指出的是:上文提到的常规优化甚至是对一些算法的优化,在编译器优化前后的效果可能是相反的,换句话说,设置编译器优化选项之前,常规优化和算法优化一般都是有效的。但是,设置编译器优化选项后,之前所做的优化有可能不起作用,甚至起反作用。这里还是比较纠结的,造成这种现象的原因可能是对一些算法的经典改进本质上还是实现了常规优化算法,而编译器优化时会自动实现常规优化。举个例子:设计直线绘制函数时,考虑利用直线的对称性,在一次循环体内绘制两个像素。开编译器优化前,效率得到提高;开编译器优化后,效率反而降低。
iii)、使用缓存
优化到330ms之后,在这里停了好长时间,有一个填充算法耗时比较长,大概占200ms的样子,但是这个填充算法我一直没想好怎么设计,也就暂时用一个简单粗暴但耗时的算法替代一下,尽管如此,不考虑填充运算,仍然离目标很远。这时候和师傅聊天,他提到之前使用DSP碰到的缓存问题,我突然想起来,每次初始化内核的时候,貌似都把缓存关掉了。跑去实验室一看,果然如此,开启缓存之后,又是一个惊喜:330ms变成了33ms。但是画面存在一些问题:画面出现了很多黑点,也就是说,本来应该填充的像素没有成功填充。
查阅了arm cache相关资料后,怀疑和cache工作模式有关系,cache默认工作模式是write-back模式,而不是write-through。关于chace的工作模式,本文不详细叙述了,大概说一下:write-back模式下,cache和外存不是同步更新的;write-through模式下,在操作cache的同时,同步操作外存,这样以来,速度较write-back模式要慢一些。了解了二者差别之后,就知道对于我的项目而言,必须要保证帧存的成功写入,所以要将cache工作模式设置为write-through。查阅了ug585,arm cortex-a series programmer guide以及arm architecture reference manual发现cortex-a9的L1缓存工作模式是固定的,无法更改,L2缓存工作模式可以更改。
查阅ug585,研究与L2缓存相关寄存器,发现偏移地址为0x00000F40的reg15_debug_ctrl寄存器,可以控制L2缓存的工作模式。reg15_debug_ctrl寄存器定义如下:
在初始化L2 cache时,将reg15_debug_ctrl bit1(DWB)置1,就可以将L2 cache设置为write-through模式了。
将L2 cache设为write-through模式,再将L1指令缓存打开,将L1数据缓存关闭,屏幕坏点就消失了。但是由于关闭了L1数据缓存,作图时间也提高到了49ms左右,其中填充算法占了44ms左右,由此可见我的那个填充算法不靠谱,最起码改进空间很大。
iv)、填充算法的改进
这个算法的改进主要涉及计算机图形学的知识,我不讲算法,只将和优化相关的一些思想。起初我使用的是边标志填充算法,该算法主要有3个步骤:
a)、进行标志填充,将边界和一些分界线填充相应的标志颜色;
b)、逐行或逐列扫描充范围内每个像素,读取该像素的颜色值,判断该像素是否是标志;
c)、根据读取到的标志决定该像素的填充颜色;
这个算法主要有以下缺陷:
a)、对同一个像素进行反复读写操作,由于像素的颜色信息,存在外存中,访问时间相对较长,所以会影响效率。
b)、在扫描转换时,需要双层循环,众所周知循环的嵌套对效率的影响也是很大的。
针对上述缺陷,我放弃了边标志填充算法,使用了一维数组将边界的纵坐标保存起来,逐行扫描时读取对应纵坐标的值,对于不固定的填充边界,在扫描过程中实时计算,这样以来,通过数组保存边界信息使得双层循环变为单层循环;与此同时,对同一个像素的操作也只进行一次写操作。
由于是评估A9的作图性能,项目也没有具体要求,师傅说50ms也还可以了,主任说是要控制在33ms之内,也就是一秒钟至少要显示30幅画面。我也是想做到主任的要求,做了算法改进后,觉得能进30ms就满意了,没想到新的算法,将填充时间控制在个位数……这样一来总的作图时间控制在10ms左右,也算是一个惊喜吧~
4、结束语
总的来说,Cortex-A9的程序优化不是很难,但是编译器自动优化带来的不可控性,给优化工作带了一定的挑战——通过实际运行才知道所做的优化是否有效。但所有的优化手段还是离不开最常用的优化方法,所以我们在平时编码的时候,养成良好的编码习惯,多编写编译器“喜欢”的代码,将优化方法融入自己的编程风格中,开发必将事半功倍!
版权声明:
本文由博主“cuter”发布。欢迎转载,但不得擅自更改博文内容,也不得用于任何盈利目的。转载时不得删除作者简介和版权声明。如有盗用而不说明出处引起的版权纠纷,由盗用者自负。
博客官方地址:
ChinaAET:http://blog.chinaaet.com/cuter521