分类: 嵌入式
2009-09-28 13:46:58
嵌入式系统程序可移植性设计及性能优化之三
――――函数设计
【摘要】本章介绍了函数设计的一些基本规则。合理对各种参数进行封装,不但有利于模块的交互,更能够减少参数提高函数调用性能。其次介绍了模块划分的原则,如何减少模块间的耦合度。接着分析了宏函数中参数的基本规则、多宏语句的基本规则、宏和内联的区别以及如何防止宏参数宏语句扩展后的异常效应。最后介绍了如何利用const来进行输入参数的修饰及如何提高程序的可靠性。
【关键词】嵌入式,可移植性,函数设计,模块划分,耦合度,内聚性,内联,宏参数,const,输入参数,可靠性
3 函数设计... - 15 -
3.1 避免过多函数参数,提高调用性能... - 15 -
3.2 合理设计模块,减小耦合度... - 16 -
3.3 用宏函数提高时间效率... - 18 -
3.3.1 宏参数的基本规则... - 18 -
3.3.2 宏语句的基本规则... - 18 -
3.3.3 宏的副作用... - 20 -
3.4 Const修饰输入指针参数... - 21 -
1 函数设计
1.1 避免过多函数参数,提高调用性能
在函数设计时通常需要传递参数,为了提供函数调用的性能,某些处理器如ARM会利用寄存器来传递参数,这样不需要访问内存,其访问效率更高。但寄存器传递的参数数目有限,对于ARM体系是四个,当多于四个时,剩余的参数需要用栈传递,参数的出栈入栈都需要时间,调用性能下降。当参数之间紧密相连且通常需要在多个模块中联合使用时,应对参数进行封装,这样便于参数的传递和变量的管理。
如:SYS_STATUS DCCM_SetSfnTsn(u16 u16SfnPeriod, u16 u16TsnPeriod, u16 u16Sfn, u16 u16Tsn, u32 u32TodPart1, u32 u32TodPart2)
函数六个参数,对这些参数的解析在函数内部又要定义六个变量与之对应,这给变量的定义个管理带来了不便,如下:
u16 g_u16DCCMSfn;
u16 g_u16DCCMTsn;
u16 g_u16DCCMSfnPeriod;
u16 g_u16DCCMTsnPeriod;
STRU_DD_TOD_INFO g_struDCCMTod;
因此应将相关联的变量封装为结构体,优势在于:
a) 便于管理、定义、声明,避免零散的变量;
b) 意义明确,结构清晰;
c) 函数调用时避免传递过多参数,提高函数调用性能,参数少不易出错;
不足在于对于结构体的访问效率不如单独的变量,但此性能影响很小;为了代码更好的可读性、可移植可维护性性和可靠性,此处结构体的形式更合适。
MAT、CCM及MCP都需要用到此类信息,因此应单独提炼出结构体便于各模块的交互;模块间进行通信时要遵循一定的协议,即数据交互时要按照一定的数据格式进行传输,为防止各模块对格式的认识不统一,最好的方式是提供统一的数据结构的定义来进行数据收发
BPP端封装为STRU_BPP_TOD_SFN_TSN_PARAM
typedef struct tag_STRU_BPP_TOD_SFN_TSN_PARAM
{
STRU_DD_TOD_INFO struTod;
u16 u16SfnPeriod;
u16 u16TsnPeriod;
u16 u16Sfn;
u16 u16Tsn;
} STRU_BPP_TOD_SFN_TSN_PARAM;
而MCP端对此结构的定义为
typedef struct tag_STRU_DD_MCPBPP_TODTSNSFN_INFO
{
u32 u32TodPart1;
u32 u32TodPart2;
u16 u16SfnPrd;
u16 u16TsnPrd;
u16 u16SfnNum;
u16 u16TsnNum;
} STRU_DD_MCPBPP_TODTSNSFN_INFO;
可以发现,原来BPP和MCP的MAT模块对时隙、子帧、TOD信息的处理并没有采取统一的方式,这样在交互时容易出现问题。并且二者分别定义,有重复劳动。
1.2 合理设计模块,减小耦合度
软件模块设计时需要合理划分模块的层次结构,提高内聚性,降低耦合度,引用全局变量会增强模块之间的耦合度。
如:
DCCM模块定义了g_u16DCCMTsn、g_u16DCCMSfn等全局变量,调用DBSP模块实现的时隙回调函数DBSP_TslotCb();
DBSP模块中DBSP_TslotCb()声明外部变量,通过全局变量引用g_u16DCCMTsn、g_u16DCCMSfn等变量,并且在DBSP_TslotCb()内部只是读取了g_u16DCCMTsn、g_u16DCCMSfn的值。
因此更改函数形式,传递参数,而非引用全局变量;不更改参数值,采用值传递
void DBSP_TslotCb (u16 u16Sfn, u16 u16Tsn)
若DBSP_TslotCb不是由定义g_u16DCCMTsn、g_u16DCCMSfn等变量的DCCM模块调用,则必须声明外部变量然后引用,这种耦合是无法避免的。
另一个典型例子是资源的申请和释放。在具备操作系统的嵌入式系统中,因为嵌入式系统的内存空间往往是十分有限的,定义过多的全局变量将导致系统内存大量减少,因为全局变量在程序的整个运行期间都占据着同块内存;另外栈空间也是有限的,若在函数内部定义大容量的数据结构时,很可能导致栈溢出。上述两种情况都需要动态内存申请释放来解决,但不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的malloc 和free 成对出现,如果你写出这样的一段程序:
u8 * function(void)
{
u8 *p;
p = (u8 *)malloc(…);
if(p==NULL)
…;
… /* 一系列针对p 的操作 */
return p;
}
在某处调用function() ,用完function 中动态申请的内存后将其free ,如下:
u8 *q = function();
…
free(q);
上述代码明显是不合理的,因为违反了malloc 和free 成对出现的原则,即"谁申请,就由谁释放"原则。不满足这个原则,会导致代码的耦合度增大,因为用户在调用function 函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入function 函数,如下:
u8 *p=malloc(…);
if(p==NULL)
…;
function(p);
…
free(p);
p=NULL;
而函数function 则接收参数p,如下:
void function(u8 *p)
{
… /* 一系列针对p 的操作 */
}
另外一些公用处理模块,为了满足各种不同的调用需要,往往在内部采用了大量的if-then-else结构,这样很不好,判断语句如果太复杂,会消耗大量的时间的,应该尽量减少公用代码块的使用,避免控制耦合。
1.3 用宏函数提高时间效率
在嵌入(inline)操作符变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的实时性能,嵌入代码经常是必须的方法。但是使用这种方法在优化程序速度的同时,程序长度变大了,因此需要更多的ROM空间。使用这种优化在宏函数频繁调用并且只包含几行代码的时候,对提高运行效率是最有效的。
函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。这种转移操作要求在转去执行前要保存现场并记忆执行的地址,转回后要恢复现场,并按原来保存地址继续执行。同时函数调用是要使用系统的栈来保存数据的。因此,函数调用要有一定的时间和空间方面的开销,于是将影响其效率。
而宏函数不存在这个问题,其只是在预处理的地方将预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,不需要额外时间方面的开销。所以调用一个宏比调用一个函数更有时间效率,在频繁调用同一个宏函数的时候,该现象尤其突出。
1.3.1 宏参数的基本规则
a) 在引用宏参数时必须括起来,如参数为A,则引用时(A);
b) 若是宏表达式,则整个表达式必须括起来,如((A)+3);
采用上述规则定义一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个,如下:
#define MIN(A,B) ((A)<= (B) ? (A) : (B))
参数和表达式都必须括起来是为了防止在宏参数替换后由于运算符优先级导致的问题,因为宏参数本身可以是表达式,而且当宏在表达式中展开时,你不知道表达式里还有没级别更高的运算
1.3.2 宏语句的基本规则
a) 对于多个语句一起构成的宏函数,必须保证在宏函数替换后其为一个完整的执行序列,即任何时候都是全部顺序执行完毕。
b) 宏语句的最后一句没有语句结束符“;”,而是在引用宏函数时添加。
如:
#define MacroA if(conditon); \
Sentence1
#define MacroB sentence2; \
Sentence3
If(condition1)
MacroA;
Else
MacroB;
则宏函数替换扩展后,如下:
If(condition1)
if(conditon)
Sentence1;
Else
sentence2;
Sentence3;
If(condition1)
{
if(conditon)
Sentence1;
else
sentence2;
}
Sentence3;
可以看出扩展后的结果和设计情况相差很远,最简单的解决情况是:任何if…else下面的语句都用“{}”括起来。这样可以保证在宏函数扩展后不会出现异常。
If(condition1)
{
MacroA;
}
else
{
MacroB;
}
If(condition1)
{
if(conditon)
Sentence1;
}
else
{
sentence2;
Sentence3;
}
但顾客是上帝,我们不能要求使用我们的宏函数的用户任何时候都用“{}”来构造if…else语句,我们要本着“严于律己,宽以待人”的原则来设计我们的宏函数,这样就可以保证无论用户是否操作规范,我们总能达到他们的要求。
可有以下两种方式:
1) Do….while(0)结构
这是宏函数最常见的方式,如:
#define DD_INIT_ISR_TIME_STATS(pstruIsrTime) do \
{ \
(pstruIsrTime)->u32IsrStartTestFlag = 0; \
(pstruIsrTime)->u32IsrMinGapTime = C_DD_WORD_VAL_MAX; \
}while(0)
本质上“{}”来保证多条语句为一个执行序列的,那么下列方式可以么?
#define DD_INIT_ISR_TIME_STATS(pstruIsrTime) { \
(pstruIsrTime)->u32IsrStartTestFlag = 0; \
(pstruIsrTime)->u32IsrMinGapTime = C_DD_WORD_VAL_MAX; \
}
看似可以,实则不然,因为宏函数的规则是调用时将添加“;”,那么上述宏函数扩展后即为
{
(pstruIsrTime)->u32IsrStartTestFlag = 0;
(pstruIsrTime)->u32IsrMinGapTime = C_DD_WORD_VAL_MAX;
};
最后一个“;”就是多余的,不能编译通过。但单独一行的“;”是可以的,其表示空语句。
这也就是为什么上面添加了一条无用的while(0),起保证宏扩展后是正确的,同时不会对代码产生任何实质性的影响。
编译阶段编译器会对上述代码进行优化,while(0)语句实际上是不会执行的,因此上述Do….while(0)结构是不会损失性能的。
2) 逗号表达式“,”
其可以保证多条语句可以作为一个整体执行。虽然是正确的,但扩展后不便于理解,因此使用较少。
1.3.3 宏函数的副作用
利用上述宏函数的基本规则求最小值的代码如下:
Temp = MIN(*p++, b);
宏函数只是将参数完全替换,即MIN(*p++, b),进行宏展开后为((*p++) <= (b) ? (*p++) : (b)),如果(*p++) <= (b)成立,则表达式的值为(*p++),但由于在(*p++)<= (b)判断过程中改变了p的值,使得此时的? (*p++)非(*p++)<= (b)中的值了,违背了?号表达式的原意。
尽管上述代码符合上面我们所说的宏参数及语句使用规则,扩展后仍然会出现问题,对于宏来说唯一的解决方案是避免宏参数为表达式,即改为:
x = *p++;
Temp = MIN(x, b);
这样带入的参数x即为一个确定的值,符合设计要求。
通常函数参数为表达式的唯一目的是为了支持链式表达,可以省去一个变量,除此之外并不会提高运行性能。同时对于参数入栈的顺序并没有统一的规定,这样某些编译器的差异会导致表达式参数值与设计的差异。因此应避免使用表达式参数。
从上面的阐述,可以看到宏有一些难以避免的问题,怎么解决呢?
内联函数是代码被插入到调用者代码处的函数。如同宏函数,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用被编译器优化。
内联函数和宏函数很类似,而本质区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销,因此其执行速度比一般函数要快。你可以象调用函数一样来调用内联函数,而不必担心会产生于宏函数的一些问题。由上面的两者的特性可知,我们可以用内联函数完全取代预处理宏函数。
内联inline函数将进行参数检查,求出参数的值后再将此值带入函数中,因此((A) <= (B) ? (A) : (B))中的A是一致的。
声明内联函数看上去和普通函数非常相似:
extern void f(u32 i, u8 u8c);
当你定义一个内联函数时,在函数定义前加上 inline 关键字,并且将定义放入头文件:
inline void f(u32 i, u8 u8c)
{
// ...
}
内联函数inline声明必须是和函数体的定义放在一起,才有效。
inline u32 function(u32 i) {return i*i;}
这样我们才算定义了一个内联函数。
当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。另外对于不支持inline内联函数的编译器来说,宏函数就是唯一的减少函数调用的途径了。
1.4 Const修饰输入指针参数
在C 语言中,大多数人的概念是"const意味着常量",但实则不然,const仅意味着:"只能读的普通变量",可以称其为"不能改变的变量",尽管这个说法似乎很拗口,但却最准确的表达了C 语言中const 的本质。const是用来修饰变量的,在定义时赋初值,在编译阶段需要的常量仍然只能以#define 宏定义!
Const只是一个修饰符,不管怎么样a仍然是一个指向u32型数据的指针
a) const u32 *a; a是一个指向常整型数的指针,此最常见于函数参数,指针可修改,但其指向的内容不能修改。
b) u32 * const a; a是一个指向整型数的常指针,指针指向的整型数是可以修改的,但指针是不可修改的。
c) u32 const * a const; a是一个指向常整型数的常指针,指针指向的整型数是不可修改的,同时指针也是不可修改的。
上述定义方式的本质在于:const在谁后面谁就不可修改,const在最前面则将其后移一位即可,二者等效
也许你可能会问,即使不用关键字,也还是能很容易写出功能正确的程序,那么我们为什么还要如此看重关键字const呢?理由如下:
a) 关键字const 的作用是为给读你代码的人传达非常有用的信息。例如,在函数的形参前添加const 关键字意味着这个参数在函数体内不会被修改,属于"输入参数"。在有多个形参的时候,函数的调用者可以凭借参数前是否有const 关键字,清晰的辨别哪些是输入参数,哪些是输出参数。
b) 合理地使用关键字const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样可以将错误扼杀在摇篮中,减少bug 的出现,提高了软件的可靠性。
c) 对于输入参数,不能将const实参传递给非const形参,因为函数无法保证在内部不改变传进来的实参值;但若声明形参为const类型的,则其可以接受任意类型参数。所以为了减少对函数调用时实参的限制,将输入参数声明为const类型是最合适的方式。
采用上面的规则设计几个函数,如下:
SYS_STATUS DHPI_Tx(u32 u32SrcId, u32 u32DstId, u16 u16LinkId, u32 u32Len, const void *pvoidData)
当你只引用pvoidData所指向的值时应该加上const修饰符,程序中若修改了编译就不能通过,可以减少程序的bug。
void DD_EdmaReconfig(const u32 * const pu32Src, const u32 * const pu32Dst, u32 u32Cnt, u32 u32EdmaChan)
pu32Src和pu32Dst都是输入参数,同时pu32Src和pu32Ds本身不可修改,这就保证了只能读取固定位置的值。
若如此声明
void DD_EdmaReconfig(u32 * const pu32Src, u32 * const pu32Dst, u32 u32Cnt, u32 u32EdmaChan)
上述形式函数参数则可能为输出参数,只能读取或者更改pu32Src和pu32Ds位置的值。