?摘? 要:重点讨论了用C语言进行DSP软件设计时的一些常用的编程优化策略,旨在实现代码的高效和运算速度的提高。详细阐述了这些优化策略的特点、应用规则和性能分析。这些策略同样适用于C++开发环境。同时给出了程序设计实例。
????关键词:C语言;DSP;优化
1? 引言
??? DSP(Digital Signal Processor,数字信号处理器)是一种具有特殊结构的微处理器。自20世纪80年代初诞生以来,DSP在短短的十多年间里得到了飞速的发展。随着DSP性能价格比和开发手段的不断提高,DSP已经在通信和信息系统、信号与信号处理、自动控制、雷达、军事、航空航天、医疗、家用电器等许多领域得到了广泛的应用。
??? 与单片机相比,DSP多用于算法比较复杂、乘加运算量比较大的应用,如通信、雷达、音视频处理等。为了追求代码的高效,过去一般用汇编语言来编制DSP程序。随着DSP应用范围不断延伸,应用的日趋复杂,汇编语言程序在可读性、可修改性、可移植性和可重用性的缺点日益突出,软件需求与软件生产力之间的矛盾日益严重。引入高级语言(如C语言、C++、Java),可以解决该矛盾。在高级语言中,C语言无疑是最高效、最灵活的。各个DSP芯片公司都相继推出了相应的C语言编译器。
??? 鉴于DSP应用的复杂度,在用C语言进行DSP软件开发时,一般先在基于通用微处理器的PC机或工作站上对算法进行仿真,仿真通过后再将C程序移植到DSP平台中。
??? 按照软件开发的顺序,相应的优化工作包括两个部分:一是仿真环境中的优化,二是DSP目标环境中的进一步优化。本文主要探讨的是前者,给出了在DSP开发环境下有效C语言编程的策略,以获得最高效的编译代码;并针对策略,设计了具体实例,并比较了不同策略在TMS320C54x CCS(v1.2)和TMS320C6000 CCS(v1.2)环境下编译的结果。
2? DSP开发环境下有效C编程的策略
??? 基于通用微处理器的PC机环境中的优化工作是针对C程序的通用特性来考虑的。这方面的优化工作主要包括数据类型选择、数值操作优化、快速算法[6]、变量定义和使用优化、函数调用优化、程序流程优化以及计算表格化[6]等。
2.1? 数据类型
??? 标准C语言提供了丰富的数据类型整型、浮点、枚举、指针、结构、联合等。编程面对的问题是使用怎样的数据类型使编译生成的代码小、效率高。
??? 整型有signed和unsigned之分,分别称为char,short int,int,long int,enum。ANSI C没有规定每个类型的大小,它只是声明short int不大于int,long不小于int,enum和int具有相同尺度。这种模糊的定义影响了程序由一个处理器向另一个处理器的移植操作。为了避免这种影响,比较良好的编程风格是将数据类型按类型定义(typedef)在一个头文件中,当移植时只需要更改头文件即可:
??? 使用浮点数是非常危险的,除非系统有浮点协处理器或专门针对浮点设计的处理器,使用浮点变量会使编码尺度膨胀。即使使用协处理器,消耗时间也很多。同时,浮点数的存储空间是可变的。IEEE单精度浮点需4B,双精度需8B,扩展双精度需10B。一些小的CPU的交叉编译器仅支持单精度浮点型。
??? 避免浮点操作的方法一般是采用浮点运算定点化,用定点函数运算替代浮点操作。对于定点DSP的操作,应仔细考虑硬件的限制。同时,在编程时要大致估计数据的范围,做到所采用的数据类型刚好满足要求,并尽量做到在一个CPU指令周期内完成数据载入。
??? 由于DSP常采用可变的定位方式,结构的不适当声明会浪费很多RAM和ROM空间。如左图示例中两种不同声明方式,用sizeof()分析,在C54x中分别为14和12(单位为字[4]),在C6201中分别为56个字节和40个字节,可见不适当地声明会导致内存空间的浪费。
??? 当然,一些高性能的编译器,可根据内存空间优化各个变量的位置,但此时变量存储的次序可能和它们定义时的次序不同。
2.2? 数值操作优化
??? 对数值操作优化,主要特别注意以下几点:
??? (1) 用比特的移位操作来代替2次幂整数的乘除法运算更为有效;
??? (2) 用查表法代替三角函数运算。特别是在FFT等程序中,同时将一些运行时计算的参数做成查找表或常数数值,这样可以将运行时的计算转化为编译时的计算,从而提高运算效率;
??? (3) 当使用浮点设备时,尽量使用浮点数据类型,这能够减小定点处理单元的负担;
??? (4)尽量避免数值的上下溢出,除非是算法本身的需要。
2.3? 变量定义及使用优化
??? C语言把局部变量放在堆栈中,这种访问是间接的,因此较慢。更为有效的方法是将变量放在堆(heap)中,有两种方法实现:一种是声明为全局变量;另一种是声明变量为static。同时,要注意提高全局变量的重复利用率。
??? 对于需要多次重复访问的变量,如for循环中的变量值,一般可以设置为register变量。声明变量为register能够提高效率,但必须小心使用。在某些编译器中,优化器会自动分配一些变量为register。
??? 在C语言程序中指针和数组是可以相互替代的。对数组的寻址是非常耗时的,特别是多维数组。因此,首先应降低数组的维数,再指针化。同时,配合DSP中寻址机构所支持的增量寻址,效率会大大提高,代价是降低了可读性。
2.4? 函数调用
??? 函数调用往往产生大量代码。当C调用一个函数时,它首先把参数传递给寄存器或堆栈。如果函数参数很多,则调用开销将很大。此外,还需大量堆栈空间。最坏的情况是函数参数传递的是结构,编译器在调用函数时必须首先复制整个结构到堆栈。此外,若函数返回的是结构,调用程序保留堆栈空间,传递结构地址给函数,调用函数,然后函数返回。最后,调用程序还要清除堆栈,并将返回的结构复制到另一个结构。代码和堆栈的开销将是惊人的,特别是资源有限的DSP或其它片上嵌入式开发系统。为了避免这种开销,应禁止传递结构,一般用结构指针替代。如果结构是不可修改的,可用常量结构指针替代。
??? 函数调用的另一方面开销是局部变量。这些变量定位于堆栈,因此增加了对堆栈的要求。如果这些变量需被初始化,则在程序每次被调用时均需做一次初始化。可以这样说,限制局部变量的数目也就是对堆栈空间的限制。
??? 第三方面的开销是调用函数的返回值。如果要返回值,一般需要在函数返回前复制返回值到返回位置,然后把结果复制到调用程序中。如果函数的返回值赋值给一个变量或很少使用,可以考虑传递指向返回值的指针。被调用的函数可以直接改变返回值。
??? 对于用C++开发的用户,采用inline技术可以完全消除函数调用的开销,然而这增加了目标代码的大小。在这种情况下,应根据实际采用的编译器判断优化后程序生成的代码是否增加不大。
2.5? 程序流程设计
??? 在C语言中,程序流程控制有if…else,switch…case,do…while,for,while等,它们的使用不当也会影响程序生成代码的大小和效率。下面,本文将分别分析使用判断选取控制语句和循环控制语句时应该注意的事项:
??? 在使用判断选取控制语句时应减少判断转移。DSP多采用流水线结构。如TMS320C54X中就采用了6级流水线结构,频繁的转移指令将使流水线难以发挥作用。另外DSP的大多数指令为单周期指令,但转移类指令却通常要耗费较多的机器周期。因此,应尽可能减少程序中的转移分支。一般通过对程序流的分析,许多判断转移可以用简单的条件组合来实现。
??? 当有多种选择时,switch…case语句可读性强,然而它会带来很大开销,if…else语句更灵活,但它需要更多的C代码。if…else语句在实际的编译中可能会更为有效一些。另外一个需要考虑的是switch…case语句中参数可以是任意的整数类型,然而,若这些整数在case语句中生成一系列整数,如enum类型,许多编译器将产生跳转表,这可以减少编译代码,而且平均下来,执行的效率也比较快。
??? C语言提供3种类型的循环结构:for循环、do-while循环和while循环。编译器的任务是将指令映射为处理器的指令集。许多DSP设计有零开销循环处理能力。零开销循环不需要循环计数更新、测试和回跳指令,因此能够加速处理能力。
??? 为了尽量实现循环的零开销,编译器必须知道循环的初始化、更新和结束条件。当循环表达式过于复杂或者含有的循环变量随循环体本身中的条件变化而改变量值时,许多编译器不生成零开销的循环。基于这种准则,循环表达式应写的尽可能清楚。并且尽量地对表达式做预处理,如将常数表达式移出循环,预先计算结果等。如下给出典型情况下的分析结果:
??? 虽然在循环中字符串的长度没有改变,函数strlen()却被调用了strlen(s)+1次。编译器不能优化这种多余调用函数的情况。一般情况下,一次函数调用返回一个不同的值,循环体对函数的结果产生影响。一种较好的编程思路是不对中间变量进行存储,循环改为for(i=strlen(s)-1;i>=0;i--)。
3? 结语
??? 以上从多方面探讨了用C语言开发DSP软件时的一些优化考虑。为了最有效的用C编程,应该按“程序是怎样在汇编语言中执行”的思想来编程。随着实时操作系统、嵌入式操作系统、可视开发环境的引入,以及以DSP为平台的C编译器的功能不断完善,用C开发DSP应用将更便捷。