第一章 C6000系列DSP的体系结构简介
TI的C6000系列DSP内部采用的哈佛结构的体系结构,其数据段和代码段是分开存放的并且独立编址,减轻了程序在运行时的访问存储数据的瓶颈。其中C62和C64系列均为定点的DSP处理器,而C67系列为浮点的DSP,本文主要介绍C64系列的优化方法。
C6455的功能模块图如下:
图1:c6455的功能方框图
C64系列DSP有两个数据通道,如下图:
图2:C64x的数据通道
由上图可以看出C64x有两个数据通道:Data path A和Data path B。其中每个数据通道有32个32-bit寄存器、一个乘法运算单元.M、一个地址产生单元.D、一个逻辑运算单元.L和一个移位运算单元.S。其中,逻辑运算和移位运算并不是单独在.L和.S单元完成的。它们是由交叉使用的,逻辑运算有可能会用的.S单元,移位运算也有可能用到.L单元。这8个运算单元在一个指令周期内是可以并行工作的,所以说C64x的最大优势就是可以将串行的程序并行处理。两个数据通道共有5个条件寄存器,其中通道A有3个,通道B有2个。在两个数据通道之间还有一个数据交叉单元,被称之为.X,在一个运算单元特别繁忙而另外一个运算单元比较空闲时,较忙的运算单元可以通过.X单元来访问另外一个数据通道的运算单元。当然,在实际的优化过程中应该尽量减少.X单元的使用,因为每个指令周期只能做一次数据交换,而A和B两个通道的运算是可以并行的。在优化C程序的过程中要充分利用C64x的这个特点,变串行的程序为并行的程序,加快程序的速度。也要充分利用一个.D单元可以存或者取一个32-bit数的特点,将两个16-bit合并为一个32-bit来存取,减小.D运算单元的压力。
第二章 多文件代码的代码结构和数据结构调整方法
对于一个比较大的程序来说,代码的结构和模块化的合理性对代码的优化非常的重要。较好的代码结构有利于优化人员对代码的理解,有利于对数据流的分析和输入输出关系的确定。下面就来介绍一种比较好的代码结构。
模块的划分需要对算法有一定程度的理解,所以在整理代码之前最好能够阅读程序文档和算法文档,对算法和代码有一个比较清晰的认识。阅读代码时,推荐用Source In Sight和VC Debug结合的方法来阅读代码。Source In Sight可以清晰地查看程序的函数调用关系和数据嵌套关系,VC Debug可以方便的分析程序的算法流程。
同时,模块化也将有助于对算法的理解。下面介绍一种比较流行的文件组织形式。
图3:文件的组织形式
如图3所示,最上层的是所有的C源文件,往下一层是各个C文件的头文件,再由最底层的interface.h文件来将各个C文件和头文件连接在一起。每一个C文件都首先包含了interface.h,然后包含本身的头文件。其本身的头文件中仅仅声明它上一层的C文件要用到的数据类型、表格,不存在交叉的文件包含情况。如果有别的文件要调用本文件的函数的话,需要在interface.h中声明。即interface.h中声明全局的数据类型、静态表格和函数。这样,代码就呈现出一种清晰的三层结构,其调用关系变的十分的清晰简单,不会出现重复编译的情况。而C文件的划分则是通过模块来进行划分,这需要具体的算法具体分析。
数据结构的调用关系则也是类似文件的组织形式这样一个三层的嵌套关系。
图4:结构体调用关系
以一个感知音频编码程序为例,首先底层是一个贯穿于整个程序的结构体,其成员由诸如编码速率、采样率,声道数之类的编码参数和各个模块的结构体组成;而各个模块的成员也可以根据模块的复杂度再进行嵌套。这样可以形成一个从最底层开始的按模块划分的层层嵌套的数据结构组织形式,这种的嵌套方式结构清晰,又避免了各个模块中重复定义全局变量,也避免了循环嵌套等复杂的数据嵌套关系。
建议程序设计人员在开发代码时就按照这样的一种文件和结构组织形式来开发代码,避免了后期封装带来的不便,但是如果时间上不允许可以先以功能为主,后期维护再进行程序结构上的调整,毕竟release版本是不存在任何的文件信息和符号信息的。
第三章 软件的C64x平台优化的基本方法
任何嵌入式软件在应用之前都要进行平台的优化,软件的开发人员一般不会考虑到程序的运行效率和平台的问题,所以软件的移植和优化就显得尤为重要。由于现在大部分的平台都支持标准C,也有相应的GUI的开发工具,所以软件的移植则相对比较简单,但是软件的平台优化却需要视平台的不同而进行。C64x是定点的DSP,其浮点运算都是通过函数调用的方法来实现的,所以在移植之前首先要进行程序的定点化处理。TI为了方便嵌入式开发人员的开发发行了CCS开发工具,方便了开发人员的开发,下一章将简单介绍一下CCS的编译环境配置方法,本章将把重点放在代码的优化上。
C64x软件优化技术的核心思想是串行程序的并行化。
图5:程序员和编译器之间的关系
总的来说优化可以总结为一下几条:
1、去除代码之间的相关性
2、对循环的处理方法
3、使用对齐的内存操作
4、使用intrinsics
5、使用位宽大的操作
6、软件流水线的优化方法
下面我们来逐一详细研究这几个优化方法。
一、去除代码之间的相关性。
这包含了避免内存指针之间的依赖和避免数据冲突两个内容。其中最常用的方法是避免内存指针之间的依赖,这通常也是开始优化后的第一步,也是效果最明显的方法之一。
编译器在进行程序的优化时首先保证程序运行的正确,再尽量的去优化程序。例如:
void fun1(int *pSrc, int *pDst)
{
pDst[0]=pSrc[0]+pSrc[1]; //286
pDst[1]=pSrc[2]+pSrc[3]; //287
}
经过CCS编译后的循环体汇编代码如下,可以看到一次循环需要15cycyles。
LDW .D1T1 *A4,A3 :286
LDW .D1T1 *A4(4),A5 :286
NOP 4
ADD .D1 A5,A3,A3 :286
STW .D2T1 A3,*B4 :286
LDW .D1T1 *+A4(8),A5 :287
LDW .D1T1 *+A4(12),A3 :287
RETNOP .S2 B3,3 :288
ADD .D1 A3,A5,A3 :287
STW .D2T1 A3,*+B4(4) :287
其中第三行NOP 4进行了4个指令周期的空操作来等待LDW .D1T1 *A4(4),A5的执行完成。为的是确保已经将pSrc[0]和pSrc[1]读取到寄存器A3和A5中。而分析程序我们知道pSrc和pDst是不重叠的,pDst的改变不会影响到pSrc的内容,所以可以把fun1()改写为以下形式:
void fun2(int* restrict pSrc, int* restrict pDst)
{
pDst[0]=pSrc[0]+pSrc[1]; //286
pDst[1]=pSrc[2]+pSrc[3]; //287
}
从下面的汇编代码可以看出,一个循环体之需要9cycles,“||”表示本条指令和上一条指令是并行运行的。
LDW .D1T1 *+A4(8),A6 :287
LDW .D1T1 *+A4(12),A5 :287
LDW .D1T1 *+A4(4),A3 :286
LDW .D1T1 *A4,A4 :286
RETNOP .S2 B3,2 :288
ADD .D1 A5,A6,A5 :287
STW .D2T1 A5,*+B4(4) :287
|| ADD .D1 A3,A4,A3 :286
STW .D2T1 A3,*B4 :286
可以看出,CPU一次性读进了四个数,然后再进行运算,减少了CPU的等待时间,这样就缩短了运行周期。
由上面的例子可以看出,如果在一个函数内部,能够肯定两个指针指向的内存之间没有重叠,则定义指针时用restrict关键字修饰。这样可以在很大程度上提高运算速度。或者使用const关键字修饰,将指针声明为只读,其效果和restrict效果相同。
C64x在一个时钟周期内,访问处于同一个MEM BANK中的两个数据会产生冲突,其中的一个数据访问会被延迟。因此,在设计是应尽量使函数内部需同时处理的两个数据放在不同的MEM BANK中。具体方法为用DATA_BANK修饰,在定义全局数组之前用#pragma DATA_BANK(data, bank)声明,bank取值为:0,2,4,6 (8字节对齐),data为全剧数组的数组名。这种方法仅用于全局数组的声明。举例如下:
int data1[128];
int data2[128];
int data3[128];
void fun1()
{
int k;
for(k=0;k<128;k++){
data2[k]=data1[k]+data3[k];
}
}
上面的函数一次调用需要1076个指令周期,而用#pragma DATA_BANK声明以后如下:
#pragma DATA_BANK(data1,0)
#pragma DATA_BANK(data2,2)
#pragma DATA_BANK(data3,4)
int data1[128];
int data2[128];
int data3[128];
void fun1()
{
int k;
for(k=0;k<128;k++){
data2[k]=data1[k]+data3[k];
}
}
优化后的程序仅仅需要830个指令周期就可以完成一次函数调用。
二、对循环的处理方法
对于循环的处理,关键在于一个字――“拆”。就是将多重循环尽量拆分为单层循环,因为C64x的编译器只会对内层循环进行优化。而对于单重循环程序员只需要给编译器提供循环次数、字节对齐等信息,建议不要手工展开。手工展开的效果和编译器展开的效果相同。例如:
for(i=0;i {
int sum=0;
for(j=0;j<8;j++) { sum=sum+val>>j;
}
}
这个for循环有两层嵌套,内层的for循环不仅次数可以确定,而且循环体运算简单,应该将其手工展开:
for(i=0;i>1;
sum=sum+val>>2;
sum=sum+val>>3;
sum=sum+val>>4;
sum=sum+val>>5;
sum=sum+val>>6;
sum=sum+val>>7;
}
这样一个二重循环就变成了一个单重循环,且循环体内没有打断流水线的语句,有利于编译器的优化。
由于C64x对循环的优化能力尤其强大,所以在能够用到循环得地方尽量用循环来实现,尽量多的给编译器提供循环次数信息,同时也要让循环语句的表达式尽量简单,这样编译器可以正确地判断哪个变量是循环变量,有利于软件流水线的优化。定义局部变量时,局部变量的声明周期尽可能小,这样可以减少对寄存器资源的占用;循环体内使用的局部变量不要定义在循环题外面,外部传入的指针及参数除外。
循环是最能体现C64x的软件流水线的强大功能的,也是最能体现流水线优化能力的,所以在软件流水线的优化上也将大量涉及到循环的优化方法,稍后在讲解软件流水线时再着重介绍。
三、使用对齐的内存操作
DM642的硬件结构不仅支持对齐的内存操作,也支持不对齐的内存操作。但是在DM642中对齐的内存操作的效率要高于不对齐的内存操作,其不对齐的内存操作实际上是通过了两次对其的内存操作来实现的。因此,在程序中应该尽量使用对其的内存操作,在送给编译器的信息中也尽量得告诉编译字节对其信息。优化时可以使用_nassert()和其他内存指令通知编译器,例如:
void fun1(int *restrict pSrc, int *restrict pDst)
{
_nassert(((int)pSrc&7)==0);
_nassert(((int)pDst&7)==0); //两块内存都是8字节对其
int k;
for(k=0;k<40;k++){
pDst[k]=pSrc[k];
}
}
void fun2(int *restrict pSrc, int *restrict pDst)
{
int k;
for(k=o;k<40;k++){ _amemd8(pDst+k)=_amemd8(pSrc+k); //强制使用对齐的内存操作; } }
以上的两个函数使用了不同的内存指令,其效果是等价的。但是,需要注意的是在实际的程序优化中,在使用内存对齐指令时一定要保证其内存是字节对齐的,否则程序会出错。例如假设下面的函数中数组a[]是字节不对齐的,我们想要在屏幕中输出a[1],比较以下两个函数:
fun1() { int a1_a0; short a[]={1,2,3,4,5,}; a1_a0=_amem4(&a[1]); printf("a[1]=%d",a1_a0>>16);
}
这个函数最终的输出结果为a[1]=1。
fun2()
{
int a1_a0;
short a[]={1,2,3,4,5,};
a1_a0=_mem4(&a[1]);
printf("a[2]=%d",a1_a0>>16);
}
这个函数最终的输出结果为a[1]=2。因为a[]在内存中是字节不对齐的,如果在使用过程中强制使用字节对其的读取方式,则读到寄存器中的将是一个字节对其的32-bit数,而不是我们想要的a1_a0,程序就会出错。所以在代码的优化过程中一定要弄清楚此块内存是不是字节对齐的,是几字节对齐的。
四、intrinsics的使用
DM642支持SIMD(Single Instruction Multiple Date,单指令多数据)指令,其intrinsics其实就是SIMD汇编指令的C语言组织形式。使用intrinsics会大大提高程序的运算速度。C64x的intrinsics提供了大量对short和char数据的快速运算指令,如:_sadd2(), _smpy2()等。所以,如果能够在程序中使用short或者char数据类型的应该首选这两种数据类型,可以方便程序使用intrinsics。如果能够使用intrinsics的地方尽量去使用它,它可以将C几个指令周期才能完成的工作在一个指令周期内完成,大大的提高了程序的运行效率。
下面对intrinsics的使用举一个简单的例子:
32位饱和加法:sum=_sadd(a,b);
16位饱和加法:sum=_spack2(a+b,0)>>16;
下面列举一些在优化过程中比较常用的几个intrinsics指令:
_sadd(a,b) 32位饱和加法;
_sadd2(a,b) 两个16位饱和加法,高16位和高6位相加,低16位和低16位相加;
_ssub(a,b) 32位饱和减法;
_sub2(a,b) 两个16位减法;
_smpy(a,b) 32位饱和乘法;
_smpy2(a,b) 两个16位饱和乘法;
_max2(a,b) 找出高16位的最大值和低16位的最大值,再将两个最大值放到一个32-bit数中返回,也适合找出两个short型数的最大值,如果有较多的if(a<30) a=30之类的语句,可以直接用a=_max2(a,30)来代替,节省一个条件寄存器;
_min2(a,b) 找出高16位的最小值和低16位的最小值,用法和_max2(a,b)相同;
_norm(a) 找出除符号为以外a中第一个非0位的位置,位置的表示方法是从高位往低位数;
_bitr(a) 比特反转;
_abs(a) 取a的绝对值;
_spack2(a,b) 将两个32-bit数先进行16位饱和,然后把饱和后的a放在一个32-bit数的高16位,饱和后的b放在低16位上返回;
_rotl(a,b) 将b循环左移a位;
_subc(a,b) 如果a-b大于0,则返回(a-b)<<1,否则返回(a<<1)+1;
_swap4(a) 将a中的高16位和低16位进行大小头的转换;
以上的几个intrinsics是在优化过程中比较常用的intrincics指令,实际的优化中还要根据实际的需要在使用intrinsics,并查阅Ti的相关文档。从上面列举的指令可以看出,大部分指令和其汇编指令都是相同的,而且根据指令是否有2、4等数字还可以判断这个指令是对多大的数据类型进行操作的。
灵活地使用intrinsics能够完成许多复杂的操作,例如本节开始所提到的16位的饱和控制,在C中需要用几个分支结构来完成。所以灵活使用intrinsics也是优化中很重要的方法,尤其是对于计算量比较大且数据类型较小的代码。
五、C64x中对数据类型的关注
C64x软件优化中对于数据类型的使用有几个原则。第一,尽可能避免使用int(32bits)和long(40bits)类型;第二,在进行乘法运算时,尽可能使用short*short(1 cycle),而不要使用int*int(5 cycle);第三,循环计数变量必须使用int和unsigned int,而不要使用short或者unsigned short,避免使用不必要的符号扩展指令;第四,在使用C6400设备时,要使用-mv6400编译开关,这样可以充分利用C64x的硬件资源和特有指令;第五,当处理8bits或者16bits数据时,尽可能使用unsigned char和singed short,原因是C64x中intrinsics指令对两种数据有更好的支持。
通过上面五点大致可以总结为尽量使用位宽小的数据,用位宽大的操作来实现对位宽小的数据的多次操作。例如一个复制函数,即将x数组中所有的数据复制到y数组中:
void fun1()
{
int i;
short x[n];
short y[n];
for(i=0;i {
y[i]=x[i];
}
}
可以优化为:
void fun2()
{
int i;
int temp;
int *p1=(int *)&x[0]; //将x[]声明为int,这样一次就可以读入两个short型数
int *q1=(int *)&y[0]; //将y[]声明为int,这样一次就可以读入两个short型数
for(i=0;i {
temp=*p1++;
*q1++=temp;
}
}
如果n能够被4整除,也可以一次读入四个short型数到一个double型变量中,这样程序运行效率会更高。通过上面的例子我们也可以推广到两个数组相加,两个数组点乘,以及求两个数组的最大最小值等操作,也可以结合intrinsics的使用,也可以提高运算的并行度。
六、软件流水线的优化方法
软件流水线是编译器针对循环使用的一种调度技术。使用流水线技术后,可以是多个循环体并行起来,从而加快循环的速度。其原理是,将循环体划分成多个部分,在流水线的每一个环节可同时处理多个循环体的不同部分,如下图所示:
图6:软件流水线循环
从图上我们可以看到,从一个流水线的形成,到流水线的删除一共经历了三个阶段:prolog, kernel, epilog。Prolog是流水线的形成,有4个周期;kernel是流水线的内核,是流水线的核心部分;epilog是流水线的删除阶段,也有四个周期。所以,在优化过程中应该尽量的增加流水线的kernel,减小流水线的prolog和epilog阶段。可以用强制展开等方法使流水线充分流起来。
举个例子:假设循环次数为100,在流水线上同时有5个循环在执行;不启用流水线所需的周期数=5*100=500周期;启用流水线所需的周期数=4+4+96*1=104周期。
前面已经提到过,对循环的流水线优化也是C64x软件优化效果最明显的方法之一,所以对于循环也有几点需要注意的:
(1) 尽量不要使用if等分支结构(如必须使用if…else…结构,应将大概律程序块放在前,避免了后续的判断);小型的if…else…也可以用逻辑表达式来展开。例如:
if(A)
c=a;
else
c=b;
可以改写为
c=((0-A)&a)+((A-1)&b);
C64x有条件寄存器,所以通常没有这个必要。
(2) 不能有提前终止的指令,如:return, break, continue。在此情况下,编译器是不会流水化处理的。提前终止可以用多余的计算来保证流水线的畅通,比如说用一个结构体记录提前终止时的状态,待循环完全进行完之后再通过这个结构体恢复到提前终止的状态。这种方法虽然引入了大量的多余的计算,但是保证了流水线的畅通。
(3) 不能调用函数,调用函数会打断流水线,所以需要调用函数的地方可以将函数声明成内联或者写成一个宏的形式。
(4) 不能使用浮点数(在定点DSP上,浮点运算都是用函数实现的),所以在优化之前切记应该先进行定点化。
(5) 不能使用除法(在DSP上除法都是用函数实现的),除法的的替代算法在后面一章LDX的优化方法中会介绍。
(6) 循环体不要太大,建议40cycles以下,强制展开最大120cycles。
(7) 循环体内指针尽量用restrict声明,减少CPU的等待时间。
(8) 尽量使内层循环次数多而外层循环次数少,编译器只会优化最内层循环,所以尽量应该采用单层循环来进行计算。
(9) 尽量不要在循环体内部修改循环计数变量,否则编译器将无法判断循环次数。
(10)尽可能给编译器提供各种有利优化的信息(字节对齐,循环次数,),尤其重要的是循环次数的公约数。
对于循环的流水线的优化我们可以通过分析.asm文件来进行,.asm文件反馈了编译的大量信息。一个简单的循环:
for(i=0;i<40;i++) { a+=i; } 编译后在asm文件中找到相应的行,得到循环的软件流水线信息,asm文件太长,不列举了。
流水线信息首先是循环的一些基本信息,然后是两个数据通道的是用情况,接下来是寄存器的使用情况,最后是循环内核。 对于优化来说比较重要的信息有:Loop Unroll Multiple,循环展开倍数,表示了循环被编译器几倍展开,比如2x就表示循环被2倍展开,4x就表示循环被四倍展开。Loop Carried Dependency Bound(^),循环相关性,表示了循环内核语句之间的相关性,越小越好,在循环内核中行标有^符号的语句是具有相关性的语句。两个数据通道的使用情况,这里列出了各个运算单元的使用情况,数字越大,表示使用的越多,一般减小语句之间的相关性应该是优化的过程中需要早期解决的问题。*表示了这个运算单元是编译器优化的瓶颈所在,优化人员应该设法减小这个单元的负荷;一般情况下应该是A和B两个通道的使用情况尽量平衡,如果特别不平衡可以将循环强制展开两倍。ii=1表示每次循环迭代占用1个时钟周期,用了6个功能单元,因为循环比较简单,所以这是比较好的优化结果。循环软件流水线的优化目标就是使ii尽量小,使用的功能单元个数尽量多。 紧接着是寄存器的使用情况,星号表示了在这个时钟周期内这个寄存器被使用。上例中的寄存器使用情况比较简单,是因为循环体比较简单,实际中寄存器的使用也是一个很重要的问题。我们优化的过程中应该寻求寄存器使用饱满且两个通道的均衡,最好的结果是A-side和B-side的寄存器在每一个指令周期都被使用,这说明程序已经最大限度的挖掘的寄存器的潜能,当然这是不可能。 循环内核中显示的是一次循环的汇编代码,||表示本条语句和上一条语句可以在一个指令周期内并行进行,然后是助记符,下一列是所使用的功能单元,紧接着是使用的寄存器,最后一列分号之后表示的是编译之前的行号和相关性标识。优化人员可以根据行号来找到C代码来进行细节的优化。 我们在通常的优化过程中,一般使用5个功能单元已经是很好的情况了,除了一些十分简单的循环,很难达到使用6个以上的功能单元。优化时ii值应该尽量的小,具体情况要根据循环的复杂程度来看,越复杂的情况ii值越大。 在循环的优化过程中,第一步应该是寻求两个数据通道的平衡,包括功能单元的平衡和寄存器使用的平衡,一般的方法是将循环强制展开两次,方法是使用#pragma UNROLL(2),要求循环次数是2的倍数。如果循环次数为奇数可以将循环拆分为两个循环来进行。 接下来需要观察相关性的问题,如果代码的相关性很高,就需要减小代码的相关性,方法有很多,可以将内存中的数据全部读入到寄存器中再进行计算,用restrict关键字来修饰指针或者数组指针等等。 然后观察运算单元的负载,如果带有星号的表示这个功能单元是编译其继续优化的瓶颈,应该设法减小这个功能单元的压力,在第一章已经介绍了每个功能单元的主要功能,根据它们对代码中的计算进行调整即可。注意,一般不可能将星号去掉,只能尽量的减少各个运算单元的压力,优化的目标是各个运算单元平衡,两条数据通道平衡。 最后根据得到的结果进行代码的微调。
第四章 一般编译选项的设置
略
第五章 LDX算法的C64x平台优化
一~六,略,涉及软件的具体算法。
七、除法的替代算法
在定点的DSP中,除法通常是通过函数调用的方法来实现的,因此,不可避免的打断了流水线。
如果是16位的除法,可以用一下的算法来代替:
Word16 div_s1(Word16 var1, Word16 var2)
{
Word32 var1int;
Word32 var2int;
var1int=var1<<16;
var2int=var2<<15;
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
var1int=_subc(var1int,var2int);
return(var1int&0xffff);
}
上面的代码要求为无符号除法,var1<=var2,得到的结果是Q15定标的结果。
如果是32位除法,本身的记过对精度要求不高时可以用h1/h2的方法实现。
如果除数较小,小于16位是可以用下面的方法来实现:
((h1/l2)<<16)|(l1/l2);
ti提供的除法算法(见TMS320C6000 Integer Division, Aplication Report, SPRA707_, October 2000):unsigned int udiv(unsigned int num, unsigned int den) { int i,shift; if(den>num) return 0;
if(num==0) return 0;
if(den==0) return -1;
shift=_lmdb(1,den)-_lmdb(1,num);
den<<=shift;
for(i=0;i<=shift;i++)
num=_subc(num,den);
return (num<<(32-(shift+1)))>>(32-(shift+1));
}
int sdiv(int num, int den)
{
int i,shift,sign;
sign=(num>>31)^(den>>31);
num=_abs(num);
den=_abs(den);
if(den>num) return 0;
if(num==0) return 0;
if(den==0) return -1;
shift=_lmdb(1,den)-_lmdb(1,num);
den<<=shift;
for(i=0;i<=shift;i++) num=_subc(num,den); num=_extu(num,(32-(shift+1)),(32-(shift+1))); if(sign) return -num; else return num; }
如果得到的结果比较小,且精度要求较高,可以用少量的if分支结构来实现没有循环的除法算法。 如:结果绝对值小于32,即2的5次幂。
static int map[6][3]= { {1,0,0,}, //1=1 {0,1,0,}, //2=2 {0,0,1,}, //3=3 {1,0,1,}, //4=1+3 {0,1,1,}, //5=2+3 {1,1,1,}, //6=1+2+3 } int sdiv(int num, int den) { int i,shift,sign; sign=(num>>31)^(den>>31);
num=_abs(num);
den=_abs(den);
if(den>num) return 0;
if(num==0) return 0;
if(den==0) return -1;
shift=_lmdb(1,den)-_lmdb(1,num);
den<<=shift; if(map[shift][0]) { num=_subc(num,den); } if(map[shift][1]) { num=_subc(num,den); num=_subc(num,den); } if(map[shift][2]) { num=_subc(num,den); num=_subc(num,den); num=_subc(num,den); } num=_extu(num,(32-(shift+1)),(32-(shift+1))); if(sign) return -num; else return num; }
第六章 其他的C64x优化技巧和嵌入式开发技巧
1、优化过程中如果遇到有多个出口的函数,即有多个return,尽量在逻辑上将提改为仅有一个return;将变量的读入和计算放在使用之前,减少变量的生存周期和变量对寄存器的占用时间。编译器在变量声明的时候是不会给变量分配寄存器的,只有在变量使用的时候才会给变量分配寄存器。如:
{
int a;
//…
a=val;
if(…)
return 0;
//…
ret=a;
return a;
}
上面的代码中有两个return,变量a在第一个return之前已经赋值,但是在第一个return之后才用到了变量a,应该将a的赋值放在第一个return之后:
{
int a;
//…
if(…)
return 0;
a=val;
//…
ret=a;
return a;
}
这样会在一定程度上减小代码需要的指令数。
2、在C64x中,寄存器是十分宝贵的资源,有着速度快的特点,所以应该充分利用寄存器,在多次使用同一个运算的情况下,应该首先将它计算出来放在寄存器中。对于数组中的值,如果使用的频繁可以首先将这个值读入到寄存器中,避免了大量的读内存操作。除了充分利用寄存器,另一方面也要节省寄存器的使用,给编译器留有优化的空间,将局部变量尽量声明在使用之前。
3、大的局部数组应该声明为全局数组,一方面全局数组是默认8字节对齐的,另一方面可以使用#pragma DATA_BANK声明。而且减小了寄存器的压力。而在Trimedia平台中有128个寄存器,一般情况下无需为寄存器不够用而担忧。
4、在程序中应该避免使用多维指针,减少指针寻址的次数,多维指针如果可以用比较少维数的数组来代替的话应该用比较少维数的数组代替,前提是浪费的内存并不多,比如在音频的编码算法中经常会有单声道和双声道的切换,一个二维指针**q,首先需要第一维分配声道数个一维指针,然后每一个声道再分配需要的内存。这种情况应该改为*q[2],再给需要的指针分配内存(这个方法有没有效果我深表怀疑,根据我的分析,我认为无论是二维指针和一维指针数组,其寻址的次数应该是相同的,仅仅是少了分配内存的次数)。
5、在需要实现多路编解码程序中,静态表格应当是只读的,否则如果一路编码程序改写了静态表格,另外一路读取到的就不是原始的静态表格,容易导致程序的出错和硬件的死机。
6、在嵌入式开发过程中,对于内存的操作十分重要,最重要的一点就是分配的内存一定要进行释放,否则会造成内存泄漏,导致硬件死机。能够复用的内存一定要复用。在程序的主要编码循环中不要进行内存的分配和释放工作。
7、对于大部分的小函数,声明称inline比写成宏速度快。内联函数必须声明在其被调用的函数的文件里,或者在被调用函数所在文件包含的文件中,否则编译会出错。
8、C64x下部分库函数和VC下的库函数的参数类型不一样,如floor在VC下其输入参数为float型,而在C64x下是double型,如果要实现VC下的功能,应该使用floorf。
9、在CCS中进行profile时,应该在cdb文件中的TSK选项中重新开一个任务,不要再main()函数中进行剖分,其结构不准确。具体做法一般为,将main()写成一个空任务,将之前的main()函数改为新开任务的任务名
转自:
http://www.polluxa.com/%E6%9D%82%E8%B0%88/2011/03/20110328/
今天的文章DSP优化——C6000分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/6239.html