引言 随着DSP技术的不断发展和完善,数字信号处理的应用范围越来越广泛。工控、计算机、通信和消费电子产品中,都会找到它的影子。到了20世纪80年代后期,各个DSP的生产商都推出了自己的高级语言编译器。这使得利用高级语言开发DSP软件成为可能。 编译器的原理是通过特定的语法规则把高级语言书写的逻辑转化成特定硬件平台所认知的汇编语言。由于编译器的首要性能是依据一定的规则编译出逻辑正确的代码。这样在保证正确性的前提下,编译出的汇编代码冗余很难兼顾效率。在一些实时性要求比较高的场合,例如在语音图像处理方面,必须对某些关键的算法进行优化。本文以TI公司的DSP芯片TMS320C55X为例,介绍如何对一个工程进行优化。 优化的一般步骤 在高级语言编译器出现以前,由于软件部分都是由汇编来完成,并且写出的代码性能都比较高,所以代码的优化在开发过程中已经完成,不需要把优化单独地作为开发的一个步骤。现在随着高级语言应用到DSP系统的开发中,在软件功能实现的基础上,软件执行效率的优化显得愈加重要。每个人在优化过程中使用的具体方法各有特点,但在总体上还是有一定的规范可寻。 笔者根据自己的实际工作经验,并参照其它比较成功的优化实例,总结了以下几个步骤,作为优化过程的参考。: - 向C55X上移植所需的准备工作:数据类型的定义、intrinsic函数的使用、为适合多通道的应用所做的代码的修改。
- 工程层的优化:对于函数体较小的函数使用"inline"限定词、数据的对齐。
- C函数层的优化:针对TMS320C55X系列芯片的内部结构;改变C代码使其能在硬件最大使用概率的条件下,降低算法的用时。
- 部分算法的修改。
- 部分函数的手工汇编。
向C55X上移植所需的必要工作 由于DSP硬件结构的约束,用C语言开发的代码在向其移植时,必须作相应的改动,来适应特定硬件平台的特点。总结移植所需要的工作,主要有以下几点: (1)数据类型的定义。由于C55X系列芯片是TI公司生产的定点DSP芯片,其中累加器为40位。为了实现定点小数的数学运算,定义一个typedef.h的文件,在typedef.h文件中定义了几种数据类型。Word16对应short型;Word32对应long型;Flag对应int型。 (2)Intrinsic函数的使用。由所定义的数据类型可以实现定点的数学运算,由于这些基本的计算被多次使用,所以TI公司提供了这些函数的优化汇编代码。在算法实现代码文件头中加入"#include<gsm.h>"语句,但代码中调用了这些函数,则在编译时会自动把优化过的汇编代码嵌入到输出文件"*.asm"中,从而节省了大量的时间。 (3)为适合多通道的应用所做的代码修改。在 DSP 上实现的有些算法,例如语音的编解码等,需要同时处理多个通道。由于硬件资源(内存等)在工程的设计阶段已经划分完毕,所以要求在算法内不能在有内存的动态分配。解决的方法是事先把需要动态分配内存的变量放到一个结构体当中,集中在工程的设计阶段分配好内存。例如,将有关编码需要动态分配内存的数据结构合并为一个独立的结构体。这样当有多个通道同时工作时,只要对每一个通道分别开辟一块内存,公用算法代码,就可以实现多通道的应用。 工程层的优化 在工程层的优化中一般使用以下两种技术:内嵌函数和数据对齐。 (1)内嵌函数 所谓的函数内嵌,是指用函数的本体代替函数的调用这一过程。这项技术去掉了复杂的函数调用过程来提高函数的执行效率,而付出的代价是增加了代码所占用的空间。 由评估信息来决定一个函数是否应该被内嵌。一般的原则是,那些代码量比较小、被频繁调用的函数适用于内嵌。但也要考虑其它的一些因素,包括函数传递参数的数量和类型、函数值返回的方式和数据对齐的方式。在某些情况下,当函数被内嵌以后,数据的对齐属性有可能被破坏。 实现函数内嵌的方法有以下三种: a.可以使用编译器的选项来隐含地使函数内嵌。使用“-x2-o0”编译选项可以控制用inline声明的函数;使用“--o3-oi<size>”编译选项可以自动内嵌一些函数体比较小的函数。内嵌函数体的最大尺寸由<size>来决定。 b.可以使用“#pragma inline”声明语句。为实现同一函数在不同的文件中被inline,这个函数应该单独地放在一个头文件中,同时在每一个引用它的地方加上static限定词,这样可以避免链接器在链接时生成重复的全局标号定义。 c.可以手工地用函数体替代函数调用。 (2)数据对齐 编译器要求把长型数据类型存放在偶数地址边界。在申明一个复杂的数据类型 (既有多字节数据又有单字节数据) 时,应该首先存放多字节数据,然后再存放单字节数据,这样可以避免内存的空洞。编译器自动地把结构的实例对齐在内存的偶数边界。 函数级的C代码的优化 这个阶段的重点在于充分利用C55X内部硬件的并行结构。这个优化技巧不需要全面地掌握算法,也不需要对数据流有深入的研究;只需要对C55X的内部结构和内部各个单元的使用技巧非常熟悉,就可以很轻松地掌握它。一般的步骤如下:
a.把函数从原来所在的文件中分离出来,方便更深入的研究; b.在每次函数的调用前后加入测试语句,用来输入数据和观察输出的数据; c.对函数进行优化然后测试优化后的代码,比较这时的输出和没有优化时的输出是否相同; d.把优化后的代码集成到整个工程中,然后用ITU-T的所有测试向量测试这个程序。 (1)挑选优化函数 挑选优化函数的主要标准是在工程层所作的评估数据: - 每一帧的信号处理过程中函数被调用的次数。通过这条数据可以找到那些函数体较小而且被频繁调用的函数。在工程层优化时此数据最有用。
- 函数每次被调用所执行的周期数。这条数据可以估算出所作的优化取得的效果。因为在每一帧的处理过程中,一个函数可能被调用几次,所以这一条并不能作为选择优化函数的标准。
- 处理每一帧数据函数被执行的总的周期数。这是选择优化函数组重要的数据。
(2)估计执行速度的提高程度 下面这些因素直接影响代码的优化程度:普通变量和指针变量的数量、循环的数量和维数、数据对齐的属性、数据的独立性、调用函数的数量。因为这些因素的存在,所有优化效果事先很难估计。例如,如果一个函数的循环维数比较多而且循环内部多是乘加的计算,则可利用硬件MAC单元实行并行,优化效果最为明显。如果分之语句和函数调用较多,则优化的程度很难估计而且效果一般不是很好。 (3)使用的优化方法 针对算法中耗费时间最严重的循环部分,不同的情况有以下几种方法:循环的打开、循环的合并、循环的分解。而这些方法使用的依据是C55X的硬件结构所使能的,充分利用A单元中的ALU和D单元的两个MAC单元。
a.循环的打开。这项技术的重点在于,它通过在每次循环中多使用了A单元中的ALU,增加了一次计算,从而降低了 循环的次数。同时循环体中的语句可以进行并行优化,这样就降低了整个循环体总的执行指令数量。 b.循环的合并。合并的前提是具有相同的循环次数并且循环体内数据的运算结果不会因为合并而改变。它节省了每次循环建立的时间和循环内相同变量重复寻址时间,提高了辅助寄存器的使用率。 c.循环的分解。汇编器在处理循环体比较大的循环时,不能把循环转化为RPTBLOCAL或RPTB语法,而是用分之语句实现。为避免这种现象,可以把循环体比较大的循环分解为两个或多个小的循环,同时定义一些局部变量来存储中间计算结果。这样可以使编译器更加合理的分配寄存器。 (4)用到的编程技巧 a.变量的声明尽量靠近变量第一次被使用的区域, 以使编译器确定变量的生命周期。 这样,可以合理地分配寄存器,但是可能增加堆栈的的使用。 b.使用"#pragma loop_count"语句声明,如果loop_count大于零,编译器则省略了相应的测试语句。 c.使用">>"运算符来代替一个变量的右移操作,这样可以避免一个函数的调用。 d.颠倒一个多重循环的顺序,这样可能会对下一步的计算带来方便。 算法的改变 如果做完了上个阶段的工作,还没有取得理想的执行速度,编程者可以适当地改变算法,或者在函数级优化方法上应用并行的技术。 在大多数情况下,算法的改变集中在比较小的局部范围内,最常见的,例如一个函数内。但是如果改变函数之间数据的传递类型,使得算法处理数据的时间用得更少,而由此所加入的数据转换函数的执行时间可以忽略的话,也可以在函数之间加入数据转换函数,从而改变其前后的函数的算法。 手工汇编 通常,利用手工汇编既可以提高代码的执行速度又可以减少代码的存储空间。但是考虑到编码模块的可移植性,手工汇编代码的数量要控制在一定的范围内,通常这个比率为手工汇编的C代码的行数占所有C源程序总行数的20%。 总结 以上几个优化级别的划分是参考Motorola公司在做DSP移植和优化时内部使用的方法。在这里介绍给大家,并把它应用到本人的实际工作中。事实证明优化的效果十分明显。另外,这里提供的优化的概念也适合各种DSP的工程优化,只是在具体的实现方法上依据硬件平台的不同而不同。
|