张喜俊,唐云凯
(1.中国电子科技集团公司 第四十一研究所,青岛266555; 2.西北核技术研究所)
张喜俊(工程师),研究方向为测试测量仪器软件。
C编译器是嵌入式系统程序员的基本工具,正是它将程序员的思想和算法转换成处理器可以执行的机器码。所有的C编译器都能够执行各种类型的优化。以gcc编译器为例,除了常见的-O1、-O2、-O3优化选项以外,还可以根据需要打开其他优化开关,它们的含义如表1所列。
表1 gcc编译器优化选项含义
在编码过程中,程序员还应该尽可能地为编译器提供更多的信息,协助编译器更好地进行优化。程序员与编译器主要通过C语言关键字进行交流。以C标准库函数strcpy为例,它的原型为“char*strcpy(char*dst,const char*src);” ,而不是“char*strcpy(char*dst,char*src);”。虽然它们只是相差一个const关键字,但在编译器看来却相差甚远。如果使用const修饰src,表明src指向一个常量字符串,编译器可直接从寄存器或者高速缓存中访问它们,而不必每次都从存储器中读出它们的内容。这样不仅提高了执行速度,且编译器也可捕获对src指向的字符串的修改,并提示程序员不应该修改常量字符串。
由于现代编译器已经能够出色地自动完成大多数优化工作,因此切忌盲目手动优化。在进行实际的优化之前,可以借助代码剖析工具进行代码分析,找出关键代码片段,然后再进行手动优化。使用gcov或者gprof之类的剖析器,可以找出一些有关代码的基本性能统计数据,例如每行代码的调用次数、代码覆盖率和各部分代码耗费的CPU时间等。了解这些基本信息之后,就知道了应该对哪些代码进行优化。
计算密集型应用指的是那些进行大量计算的应用,特别是与数字信号处理相关的应用。算法和数据结构的选择至关重要,它们应该是优化工作的重点。除了算法优化,在编码过程中程序员还应该尽可能地对基本运算进行优化。例如:
①如果b>0并且b×c不会饱和,那么可以使用乘法代替除法,将(a/b)>c替换为a>(c×b)。
②如果乘法运算中的乘数或者除法运算中的分母为2的倍数,那么可以使用移位操作代替乘除法。
③加法要快于乘法,如可使用(a+a+a)替换a×3。
以空间换效率,即以牺牲代码尺寸为代价换取运行效率。常见的优化策略包括循环展开、查找表、数组索引和内联函数等。
编者注:有关循环展开、查找表等内容的详细介绍请见本刊网站www.mesnet.com.cn。
2.3.1尽可能少使用全局变量
由于全局变量是全局可见的,可以在多个地方对其进行修改,因此编译器不能在寄存器中缓存全局变量的值。这样,读或者写全局全局变量时,都必须访问存储器以装载或存储它们;而且在访问全局变量时,为了保证其完整性必须确保操作是原子的,这又会增加开销。
2.3.2减小函数参数传递的开销
以ARM处理器为例,如果函数只包含4个参数或更少,每个参数的大小为一个字或更小,那么可以通过寄存器R0~R3来传递所有的参数。相反,如果参数个数大于4,那么其他的参数只能通过位于片内 RAM或者速度更慢的SDRAM中的堆栈来传递,与寄存器相比,它们的访问速度要慢很多。注意,在C++中非静态成员总是有一个this指针参数,一般通过寄存器R0传递,这样只剩下R1~R3三个寄存器可以传递其他参数。
如果某个函数参数为一个大型的结构体,那么最好将其改为传递指向这个结构体的指针。这是因为,C语言中的函数调用默认情况下是传值调用,编译器会将结构体复制到堆栈。如果传递一个结构体指针,那么只需要复制这个4字节的指针到寄存器。
2.3.3合理安排运行时代码和数据的位置
嵌入式系统中往往存在多种类型的存储器(如片内Flash、片内 SRAM 以及扩展的 SDRAM、Flash和EEPROM等),它们的访问速度相差甚远。同时,应用程序代码不同部分的执行频率也相差很大。以ARM嵌入式系统为例,异常向量表和中断处理器的执行最为频繁。为了获得最佳性能,可以将它们复制到片内SRAM中,然后执行存储器重映射将异常向量表定位到0地址。其他关键代码片段也可以利用分散加载描述文件(用于RealView工具链,gnu工具链使用.lds文件)通知链接器和加载器,将它们装载到指定的存储器地址。
优秀的代码倾向于展示良好的局部性,它包含两个方面的含义:其一,倾向于在极短的时间间隔内,多次引用同一存储器位置;其二,倾向于在极短的时间间隔内,引用当前访问的数据项附近的数据项。
具有良好局部性的代码之所以具有较高的运行效率,这是因为计算机的存储器系统是一个具有不同容量、成本和访问时间的存储设备的层次结构。嵌入式系统的存储器层次结构一般可以分为寄存器、高速缓存、主存和Flash,它们的访问时间逐步上升。层次结构中的每一层都缓存来自较低一层的数据对象,如果需要访问的数据没有位于高速缓存中(即缓存不命中),那么不得不耗费数十倍的时间从主存中获得,极大地降低了运行效率。
比较下面2种二维数组求和方法。由于C语言以行优先顺序存储数组,因此行优先累加方法的局部性要优于列优先累加方法。
[1]Sloss Andrew N,Symes Dominic,Wright Chris.ARM嵌入式系统开发:软件设计与优化[M].沈建华,译.北京:北京航空航天大学出版社,2005.
[2]Bryant Randal E,O'Hallaron David.深入理解计算机系统(修订版)[M].龚奕利,雷迎春,译.北京:中国电力出版社,2004.
[3]M Richard.Using the GNU Compiler Collection:For GCC version 4.4.1[EB/OL].[2009-10].http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc.pdf.
[4]Ghosh Koushik.Writing Efficient C andC Code Optimization[EB/OL].(2004-2-26)[2009-10].http://www.codeproject.com/KB/cpp/C_Code_Optimization.aspx.