今天大嘴主要介绍一下这些年来本人在做图像算法的嵌入式移植时常采用的优化方法,由于篇幅和时间有限,这里主要列出一个大框,具体的如果大家有兴趣可以慢慢与大嘴交流。
一. 前序
1. 图像算法在嵌入式移植时(主要针对DSP芯片)优化的原则和步骤?
原则:算法效果达到预期之前最好不要做过多的优化
步骤:windows下的算法级优化—>C语言的优化—>DSP下的C编译器优化(如软件流水等) —>C语言代码和结构的优化(如浮点转定点) —> (线性)汇编做极限优化
2. 需要对程序中的哪些代码进行优化?
大家都知道程序中最费时的莫过于循环操作了,主要是针对循环进行优化(一般我都是针对100次以上的循环进行优化,特别是复杂度在O(m*n)以及O(n^2)以上的循环优化)(关于循环中复杂度的计算方法大嘴会在以后介绍,请留意)。
二. 较详细的介绍
1. 算法级优化
算法效果达到预期之前最好不要做过多的优化,为啥?算法未稳定就草率
优化是做算法和移植的大忌,一是因为算法以后修改后你还得优化,二是各种优化后你的算法代码如果未充分注释,多日后可能连本人都无法看懂。
算法级优化举例:
大的方面:在做算法前咱们首先要衡量的算法本身的复杂度和效果,如背景建模/前景检测(即运动目标检测)采用的是“单高斯”?“混合高斯(GMM)”?“VIBE”?“codebook”?等等;
小的方面:针对某一基本算法函数优化,比如采用的sobel边缘检测,还是canny边缘检测,或者其他更好的方法。选择一个效果好又稳定的算法方向和基本函数对以后的移植和优化会有很大帮助。
再比如:你非要用神经网络做车牌识别,在PC下当然可以,但在嵌入式中却无法实时,也许有牛人真能在DSP下基于神经网络做高效又稳定的、识别率又高的车牌识别(如8168和C66系列芯片),至少在大嘴身边未发现。
2. C语言的优化
C语言本身优化是老生常谈,这里不做介绍,比如什么少用if…else之类,这里不再复述,感兴趣的朋友请自己在网络上找资料。
3. DSP下的C编译器优化
3.1设置编译选项,使编译器先自行优化
最主要的编译选项:O1,O2,O3等几个等级(如DSP开发环境CCS中自带的可选编译选项)
代码风格没有充分遵守CCS编译器的约定则会导致编译器不做优化,即使开了不同等级,也还是不会做优化,日后大嘴会逐步介绍
3.2开通软件流水(software pipelining),针对内层循环,使多次迭代并行执行
方法和注意事项
(i)方法:设置编译选项(一大堆),
是否开启软件流水,通过生成的.asm文件查看
(ii)无法开启流水的一些原因
代码风格(如过多的条件跳转指令条件寄存器个数有限,达到资源限)、错误编译选项等等
3.3使用内联inline和intrinsic函数(可归类为代码级优化)
说的不多,但展开了内容却很多,详细的还要靠大家去研究下TI提供的那几本手册(C6000优化编译器手册、C6000程序员手册、C6000汇编手册等),建议主要看英文原版,中文版(《TMS320C6000系列DSP编程工具与指南》)作为辅助(几年前阅读过,个人觉得里面很多翻译的不是很到位)。
4. DSP代码和结构的优化
4.1浮点转定点(PC和DSP下可通用)
浮点数:是属于有理数中某特定子集的数的数字表示,在计算机中用以近似
表示任意某个实数。具体的说,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这种表示方法类似于基数为10的科学计数法。
定点数:参与运算的数的小数点位置固定不变
浮点数形式上可以简单认为是float和double类型,定点数形式上可以简单认为是个整数。
(i)简单方法:在精度满足的情况下:把数据放大n倍,运算后再缩小,
(ii)但为了准确把握好精度,通常用Q定标小数点移位(关于Q定标小数点移位较复杂,大嘴以后专门介绍)
4.2算术和逻辑运算
(i)能用移位和逻辑运算的绝不用加减乘除
(ii)能用加减法的绝不用乘除法,
(iii)能用乘法的绝不用除法
(iv)能用16位(16位 * 16位= 32位结果)乘法的绝不用32位乘法,DSP有相应16位乘法器(硬件),而32位成为一般用软实现.
4.3打包操作
(i)手动:32位机,int打包,一次加载4个 byte像素(PC和DSP下可通用)
(ii)半手动:仅DSP下可用,预编译伪指令(如MUST_ITERATE),intrinsic函数,需要首地址4字节对齐
例:
#pragma MUST_ITERATE(8,32,8); //告诉编译器要采用优化(如软件流水),//循环最小次数为8,最大次数为32,循//环为8的整数倍
for(i = 0; i < 32;i = 8)
{
*pdst = _abs(*psrc);
}
d)inline和intrinsic函数
(i)inline ,语句只能几行,有各种限制
(ii)intrinsic函数,总是被内联,汇编封装,常用一些操作函数和数学运算(如求绝对值,乘法),速度很难超越,intrinsic函数不影响软件流水,TI充分考虑到这点
4.4充分利用Bit图(bit二值图),针对二值图的操作,
注意:不要和windows位图(bitmap)混淆
(1)传统的二值像素: 0和255
unsigned char A = 255;
255 == 11111111
unsigned char B = 0;
0 == 00000000
(2)基于比特图
unsigned char A;
A 10101110 (A中存了8个像素)
(3)分析:
a)基于像素操作(慢):每个二值像素占1个字节(8位,即8个bit);基于bit图操作(快):每个像素占1位(1个bit)
b)32bit包和8bit包
32bit包,可放于1个int类型的变量中,其充分利用了32位机每次加载
一个字,这样相当于一次加载32个二值像素
例:
传统的遍历每个像素(320个unsigned char像素)
for(i = 0; i < 320;i )
{
//传统的像素操作,无法并行
}
循环跳转320次,无法并行操作
基于比特图(长度为10个int型数组即可存储320个像素)
for(i = 0; i < 10;i )
{
//32bit图打包 并行操作,每32个像素可以并行执行
}
理论上提高了32倍,至少20倍以上
c)汇编->内存和寄存器、寄存器和寄存器之间频繁操作
节省大量数据搬移时间
d)Bit二值图主要针对腐蚀、膨胀、各种滤波、轮廓查找等等和二值图相关的操作,内部只采用与、或、非、移位等运算而没有加减乘除,所以超快
Bit图的所有操作写程序比较困难,一旦写成则效率大幅提升
4.5使用DMA和Cache
(1)DMA
直接存储器存取,块儿复制数据,不需要CPU 进行干预,数据直接在源地址和目的地址之间传送,不需要是中间媒介。
CSL的DMA操作,主要有3个DMA函数
DAT_copy() –> 对应C语言非DMA操作:memcpy()
DAT_fill() –>对应C语言非DMA操作:memset()
DAT_copy2d –>CSL的DMA的块拷贝,如拷贝一个矩阵
要求8字节对齐,即源地址和目的地址必须是8 的倍数
字节对齐后硬件可直接加载字,为了兼容正在迅速普及的64位机,使得你的程序可以不经修改直接编译成 64 位程序
如果没有一点发展的眼光,在现在这种 32 到 64 位过渡时期他不这样做的话将会导致太多的32位平台上开发的程序为了能运行在64位平台上不得不修改源代码。那样不仅会大大增加软件成本,还会导致人们为了不改动程序而拒绝接受64位平台。
(2)cache
Cache较小,但速度快,内层循环中最费时的部分要用L2 cache,高速缓存的速度超快。
这里简单知道下即可,篇幅有限,以后再慢慢介绍
4.6 restrict和register
int * restrict a;
int * restrict b;
restrict保证所定义的指针指向特定对象的唯一指针,即存储器不相关,否则默认编译器认为存储器相关,会影响编译器的优化
register int i;
4.7 DSP下专有的视觉库(VLIB,封装)和开源图片库(IMGLIB)
使用(VLIB,封装)和开源图片库(IMGLIB)里面包括一些基础图像处理函数,将更多的时间留给自己的其它各种优化,节省时间。
5. (线性)汇编做极限优化
如果觉得采用上述优化后算法效率仍未达到要求,最后可以采用(线性)汇编对循环内非个别语句做优化,由于本人汇编水平也比较懒,这里就不介绍了,向汇编高手们学习!
以上就是大嘴主要采用的方法,篇幅有限,实在无法更具体的展开介绍,以后大嘴会慢慢详细介绍,敬请关注,同时如果朋友还有什么更好的DSP移植优化方法,希望和大嘴交流,互相学习、共同进步!