dsp优化心得.docx
《dsp优化心得.docx》由会员分享,可在线阅读,更多相关《dsp优化心得.docx(11页珍藏版)》请在冰点文库上搜索。
dsp优化心得
iPone的一句大家都耳熟能详的广告词:
“一直被模仿,从未被超越”。
笔者认为主要是因为他们掌握着核心的算法和机器的优化策略。
因为一般的硬件我们都买的回来,但是能否将该硬件发挥到极致,就会公司之间的差别,因为同样的硬件,如果软件执行的速度不同,那结果就会有很大的差别,所以说:
真正的技术是买不来的。
所以,我们进行嵌入式开发的时候,一旦选定了DSP6000系列的芯片,就不能把它当成单片机来用,必须发挥dsp与众不同,独一无二的性能。
也就是说如何调整c语言才能够适应这么强悍的硬件就是我们考虑的重点内容,即我们应该按照哪种既定的原则去编写C代码才能够让dsp真正作为dsp在工作,发挥到dsp的优势。
dsp的优势在于:
速度!
所以,dsp的优化成为一门专业。
所以,我们一定要使自己在dsp上编写的c代码高效运行。
因为制约运行速度的因素是硬件和软件。
因为dsp一旦选定,硬件也就确定了。
所以,我们首先要注意如何提升软件的效率。
对于软件来说,一般情况下有3个优化等级。
第一:
算法上优化。
第二:
程序结构上的优化。
第三:
汇编级的优化。
我们需要的是研究前两个等级的优化。
所以,在这篇文章中,我们需要研究的重点有两个:
dsp的硬件结构和在dsp上如何优化c代码。
1.1dsp的硬件结构
关于dsp硬件结构的特色有几个:
哈佛结构,流水线结构,带宽和运算方式的高效等。
1.1.1哈佛结构
哈佛结构的本质属性是数据存储器(RAM:
存储数据的存储器)和程序存储器(存储指令)分开。
Cpu可以一边取指令,一边取数据。
这样会极大的提高处理的速度,因为以前是冯诺依曼结构,总线是分时复用的,这样会降低处理的速度。
而且,dsp6000系列是基于VLIW结构的,具体来讲就说CPU可以提取通过程序从程序存储器中一次提取256bit的指令,即CPU可以一次取8条指令放在处理中。
加上和8级流水线的配合,相当于8个传统的CPU一起工作。
1.1.2流水线结构
流水线结构涉及的CPU单元包括取指令单元(fetch),分配指令单元(dispatch)和执行指令单元(executive)。
这3个单元都是和程序存储器相关的,是处理指令的几个单元。
首先,程序总线可以一次从ROM中取到8条32位的指令,通俗点说,就说一次取了8条汇编语句。
然后经过取指令单元(4个步骤,不详述),分配指令单元(2个步骤),第一个为分配单元,作用是根据指令之间的相关性将这8条指令再次分成不同大小的执行包,如果两条指令是没有相关的,就可以同时执行,不分先后顺序。
第二个为译码单元,即翻译成可以被执行单元处理的码。
执行指令单元(根据不同的指令分为5个步骤)。
但8条指令通过不是一次这4+2+5个步骤,而是在这9个步骤中都有指令在同时工作。
例如第1个取值包在第1个步骤,第2个取值包就在第2个步骤,第3个取值包就在第3个步骤,等等。
剩下要讨论的就说最关键的部分:
执行指令单元。
执行指令单元有两个处理指令的通路,每个通路都有4个运算单元:
L,S,D,M。
所以指令运算单元就有8个。
我们所说的8级流水线作业也就是因为执行单元有8个运算单元。
以上就是软件流水线的硬件结构。
这就是最基础最最本质的硬件结构。
我们之后要做的所有的软件优化工作都将作用在这些硬件结构上。
但对于我们做优化来讲,我们不需要对硬件了解太深,但一定要了解软件流水线工作的硬件是如何工作的。
1.1.3带宽优势和运算方式优势
1.1.3.1带宽优势
DSP6000系列能够极大突破速度瓶颈的一个原因就是带宽和运算方式。
所谓带宽就是cpu一次可以访问的数据量。
我们前面提过,指令就是操作码,和指令相关的单元就说取指令单元,分配指令单元和执行指令单元。
即这些单元就说和程序存储器ROM打交道的单元。
我们在上面介绍了执行单元的8个执行指令的运算单元。
但还有32数据通路,即在一个时钟周期内可以从数据存储器RAM中读取32bit的数据。
还有32个通用寄存器(也可以是16个寄存器对)用来暂时存储操作数。
这些寄存器就是和数据存储器RAM间接打交道的硬件结构。
这里所说的带宽就是我们在从RAM中读取数据的时候,要尽量利用数据通路的宽度,即我们在编写c代码的时候,一定要“想办法”使CPU一次可以读取32位数据到通用寄存器里面(c64一次可以读取64位数据到通用寄存器对里面),因为我们一般情况下处理的原始数据都不是很大,尤其是在图像处理的时候,我们一般情况下使用的是0-255(灰度图像)或0-1(二值图像)。
即使对这些数据做加减乘除运算,得到的结果也不会超过216。
所以,我们一般情况下可以用LDW从数据存储器RAM中读取两个16位的数据(即用short声明的数据)。
所以,一般情况下,我们用short来声明一个数组,然后用LDW来一次读取32位的数据到寄存器中。
这样,我们就可以尽量利用数据通路的宽度,这就是所谓的带宽优势。
1.1.3.2运算方式优势
所谓的运算就说8个单元可以进行的操作码。
再说具体些就是:
加法,减法,乘法,除法,移位,跳转,读取,存储等等。
一般情况下,我们在发挥DSP6000芯片带宽优势的同时要注意运算上的优势。
例如,我们一次可以读取两个16位的数据放在一个32位的通用寄存器里。
然后可以用一个双16*16运算来处理这两个16位的数据,即我们可以仅仅通过做一次运算就可以进行两个通用寄存器相乘(每个寄存器中的高16位和低16位存放的是独立的16位数据,即两个通用寄存器中有4个16位数据),得到的结果就放在一个寄存器对里面。
(如A0:
A1组成的寄存器对,总长64位,即第1个结果放在A0里面,第2个结果放在A1里面)。
由于我们是在c语言层次来讨论dsp的优化,所以我们要在发挥带宽优势的时候使用内联函数。
因为我们一旦使用了字存取方式来处理数据,就必然会用到相关的内联函数,这两者是联系起来的。
这就是运算方式上的优势。
我们用数据相加来说明运算方式。
传统计算方式上,我们可以一次提取一个16位数据(放在一个32位的通用寄存器里),两次就提取两个16位数据,然后使用一个加法运算,使两个寄存器相加,结果放在放在一个通用寄存器里。
这样一次运算可以使两个16位的数相加。
如果我们在传统计算方式上进行优化的话,我们可以这样进行,首先使两个数组进行字对齐。
然后使用一个字读取的内联函数读取数据,这样可以一次读取两个16位的数据放在一个寄存器里,两次就可以读取4个16位的数据,之后再使用一个可以进行双16位数据加16位数据的内联函数来处理这两个通用寄存器。
这样的话,我们就可以一次处理两个16位数据和16位数据的相加。
下面用汇编指令来说明,MPY2可以一次执行双16bit*16bit的运算。
Thefollowingcode:
MPYA0,A1,A2
MPYHA0,A1,A3
maybereplacedby:
MPY2A0,A1,A3:
A2
这就是所谓的运算方式上的优势。
一定要记得:
带宽优势和运算方式优势是同时发挥的。
1.2dsp上如何优化c代码
在C代码基础上进行优化的主要目的就是提高代码执行的速度。
主要策略就是向dsp6000系列的编译器传递一些优化的信息。
这些优化的信息被优化器理解后,就会使编译器将c代码编译成这些优化信息对应的高效汇编指令,如使用LDW,MYPY2等字读取,双16位*16位指令,字对齐,使指令并行执行等。
我们在实际工程应用中,主要针对的就说循环的优化,因为循环是影响执行指令速度的最关键因素。
我们只要优化了循环,就完成了dsp的优化。
优化循环最大的效果就是使循环的每次迭代参加到流水线中。
下面我们来讨论优化循环的因素。
1.2.1最重要的因素:
循环次数
对于一个循环而言,循环次数是最重要的因素,如果能在保证循环功能的情况下减少循环的话(如将16位读取,16位相加改称双16位读取,双16位相加),那么将极大的优化代码。
减少循环次数将最大限度的减少该循环的指令。
因为从本质上来说,循环次数越多,跳转的次数越多,这样的话CPU处理这些跳转指令的次数越多,负担肯定越大。
循环的汇编指令必须有下面两个指令(红色标记):
LOOP:
…
循环体
…
SUBB0,1,B0//B0中为循环次数,每次执行一次循环体,B0减1
[B0]BLOOP//如果B0中减为0的话,就跳出循环;否则,转到LOOP继续执行
所以,我们在c语言中执行循环的时候,要想办法告诉编译器循环体执行的最少次数,这样的话编译器就可以不产生多余的循环。
所谓多余的循环(redundantloop)是编译器因为不知道循环的次数,所以在默认的情况下,产生两个版本。
一个是编译器假设循环次数小于最小循环次数(minimumtripcount)的情况下产生的没有软件流水的循环版本,另一个是编译器假设循环次数大于或者等于最小循环次数的情况下产生的经过软件流水的循环版本。
如果我们通过编译指示MUST_ITERATE告诉编译器循环体最少执行的次数,那么编译器就不会产生那个没有软件流水的循环版本,只会产生经过软件流水的循环版本。
在这里我们先讨论下minimumtripcount的概念。
我们在编写一个循环的时候,根据功能会编写循环体的内容,例如:
我们在循环体内执行下面的代码:
for(i=0;i<10;i++)
sum[i]=in1[i]+in2[i];
很明显,循环体内执行两个向量的相加,翻译成汇编指令有6条,也就是说一次迭代有6条指令。
如果想要将这个循环实现软件流水的话,最大的效果就说是6次迭代都参加到流水线中,形成一个循环核。
即连续六个循环体的每一个循环体中都有一个指令参与到循环核里。
这就是已经经过软件流水的循环最好的效果。
即每次都有结果输出。
这里,minimumtripcount就说6。
所以,一旦一个循环的c代码确定之后,就确定了软件流水需要的最小的循环次数minimumtripcount。
如果一次循环体中必要的汇编指令有20条。
那就是说最少要执行20次才能使8个循环体的指令都参与到循环核内。
这要的样的话,最少的循环次数就是20。
(这段话是我自己推测的,有待考证)
所以,对于一个循环来说,我们最重要的就是通知循环执行的次数。
1.2.2多层循环优化的法宝:
循环展开
两层及两层以上的循环叫做多层循环。
对于多层循环来说,优化的唯一方法就是将循环展开,不管是展开外层循环还是展开内层循环,结果就是变成单循环。
这样的话,编译器就可以优化这个循环了。
不过循环展开这个工作要我们自己去做。
循环展开最关键的是要看看哪层循环次数最小,就展开这层循环。
如下面这个双层循环:
可以看出,内层循环的循环次数很少,运算量也不大。
每个循环之占用了一个M单元用来进行乘法运算,浪费了资源。
我们可以将内层循环展开来执行,如下:
这样的话,双循环就变成了单循环,而且在每一个cycle里面使用了两个M单元来进行乘法运算。
明显的提高了执行的效率。
以后,我们在编写程序的时候,要在事先就注意编写程序,使我们需要的双层循环变成单层循环。
然后通过检查asm文件,看看资源有没有合理利用,即有没有使资源平衡使用。
有一个标准就是:
资源使用一定要平衡。
如下面所示:
ResourcePartition:
A-sideB-side
;*.Lunits00
;*.Sunits2*1
;*.Dunits2*2*
;*.Munits2*2*
;*.Xcrosspaths11
;*.Taddresspaths2*2*
;*Longreadpaths11
;*Longwritepaths00
;*Logicalops(.LS)11(.Lor.Sunit)
;*Additionops(.LSD)01(.Lor.Sor.Dunit)
;*Bound(.L.S.LS)2*1
;*Bound(.L.S.D.LS.LSD)2*2*
以上是一个循环的资源分配表。
从上面可以看出:
一个循环内D单元和M单元都用了两次,平衡了资源的分配。
两个D表示:
每次循环执行了两个LDW指令。
两个M表示:
每次循环执行了两个乘法运算。
所以,asm中的流水线信息对我们来说是个很好的参照,我们可以依次看出我们编写的c代码是否被高效的执行,是否进行了高效了流水作业。
1.2.3看懂asm文件中反馈的相关流水线信息
从前面可以看得出,asm中的信息对于我们判断编译器是否进行了软件流水相当重要。
下面,我们系统讨论下asm文件中有关的流水线信息。
`我们在编译c代码的时候用的选项是-mt,-o3,-k,-mv。
1.2.3.1内存依赖路径
我们在asm文件中搜索“SOFTWAREPIPELINEINFORMATION”,然后再看“LoopCarriedDependencyBound(^)”后面的值,如果值很大的话,那就是有依赖路径,就在被调用函数的参数列表的指针前面加上限制词:
restrict,如:
voidlesson1_c(short*restrictxptr,short*restrictyptr,short*zptr,short*w_sum,intN)。
改过之后,我们再编译,然后在观察asm中的上述参数。
1.2.3.2观察每次循环执行的时间
首先搜索“SOFTWAREPIPELINEINFORMATION”,再搜索“Searchingforsoftwarepipelinescheduleat...”,如果ii=3Schedulefoundwith5iterationsinparallel,再搜索“LoopUnrollMultiple”,如果后面的参数为2X,就说明循环被展开了一倍,而且在软件被流水之后有5个循环体同时进入软件流水(不过这个信息不是决定优化的必要参数)。
这里说明有两个循环体被同时执行。
2个循环体需要3个时钟周期,即软件流水之后每个循环体执行需要1.5个周期。
再看一个例子:
LoopUnrollMultiple:
2x
ii=2Schedulefoundwith6iterationsinparallel
说明每两个循环体执行需要2个周期,即经过软件流水之后的每个循环体执行需要1个周期(达到了极限,因为每个周期都有结果输出,即每个周期都有循环结束)。
1.2.3.3观察资源平衡—决定是否展开循环
搜索“ResourcePartition:
”,看看L,S,D,M共8个单元使用是否平衡,即是否在每个时钟周期内充分使用了资源。
先看一个资源使用不充分的例子:
第一个信息(最关键的是D单元和M单元):
ResourcePartition:
A-sideB-side
;*.Lunits00
;*.Sunits11
;*.Dunits2*1
;*.Munits11
这个信息要和下面这两个个信息一起分析。
第二个信息:
;*ii=2Schedulefoundwith6iterationsinparallel
第三个信息:
没有循环展开。
结合这三个信息发现,循环体没有展开。
每个循环体执行需要2个周期。
结合第一个信息知道,M单元两个周期只用了2次(因为每两个周期只用了两个乘法),浪费了2个M单元。
还有,两个周期内左边用了2个D单元,右边用了1个D单元,浪费了1个D单元。
注意的是:
D单元用来读取和存储数据,M单元用来进行乘法运算(对于62系列和64系列每个单元进行的运算不同,具体要看手册,例如64系列的cpu的M单元可以一次进行双16位x16位乘法运算)。
再看一个资源充分利用的情况下,仍然可以改善的情况。
第一个信息:
;*ResourcePartition:
A-sideB-side
;*.Lunits00
;*.Sunits21
;*.Dunits3*3*
;*.Munits22
第二个信息:
ii=3Schedulefoundwith5iterationsinparallel
第三个信息:
LoopUnrollMultiple:
2x
说明循环展开一次。
每两个周期需要3个周期。
这三个周期使用了6次D单元(每个周期都有读取和存储操作),使用了4个M单元,说明每3个周期都有4次乘法运算。
当D单元充分使用的情况下,我们在看看带宽使用,如果使用了LDH读取数据,就改成LDW读取数据。
这样的话就会再提高速度。
我们再看看速度达到极限的情况下,资源的利用情况。
第一个信息:
;*ResourcePartitionA-sideB-side
;*.Lunits00
;*.Sunits2*1
;*.Dunits2*2*
;*.Munits2*2*
第二个信息:
ii=2Schedulefoundwith6iterationsinparallel
第三个信息:
LoopUnrollMultiple:
2x
我们从第二个信息和第三个信息得知优化的速度达到了极限:
经过软件流水的循环执行每个循环体只有一个周期。
我们可以看到每2个周期用了4个D单元(两次读取:
LDW和两次存储:
STH),每2个周期用了4个M单元(每个周期都有两次乘法运算)。