嵌入式系统程序可移植性设计及性能优化之四――程序设计

时间:2021-08-11 22:00:27

 嵌入式系统程序可移植性设计及性能优化之四

――程序设计

 

Sailor_forever  sailing_9806@163.com 转载请注明

http://blog.csdn.net/sailor_8318/archive/<?xml:namespace prefix = st1 ns = "urn:schemas-microsoft-com:office:smarttags" />2008/07/29/2735041.aspx

 

 

【摘要】本节介绍了嵌入式系统程序设计中如何提高运行性能。通常程序的代码量和运行速度是相互制约的,性能优化需要综合考虑这两个因素。本章所述的大部分技巧对于一般的程序不会有太大的影响,但对于嵌入式系统这种对实时性有一定要求的情况下,可以考虑本文的相关技巧。现在的编译器都可以设置优化选项,一般O2优化就可以实现本文所提到的相关优化技巧,但尽量避免不同的优化选项造成的差异性,手动优化可以保证程序按照自己的意图运行。

 

【关键词】嵌入式,可移植性,程序设计,运行性能,实时性,优化级别

 

4   程序设计

4.1     循环转置

4.2     减小运算强度

4.2.1        位操作实现求余运算

4.2.2        用移位实现乘除法运算

4.2.3        将循环体内的乘法运算改成循环自加运算

4.3     减少不变计算

4.3.1        循环内部避免恒定式

4.3.2        避免结构体深度访问

4.4     减少存储访问指令周期和个数

4.5     查表

4.6     使用自加、自减指令

4.7     根据频率进行case 排序

4.8     函数指针表替代switch-case

 

 

1       程序设计

1.1    循环转置

通常使用的延时函数均采用A中自加的形式,而B中为自减延时函数

 

A 自加

B 自减

void delay (void)

{

u32 i;

for (i=0;i<1000;i++)

nop;

}

void delay (void)

{

u32 i;

for (i=1000;--i;)

nop;

}

 

两个函数的延时效果相似,但几乎所有的C编译对后一种函数生成的代码均比前一种代码少1~3个指令,因为几乎所有的MCU均有为0转移(JNZ)的指令,采用后一种方式能够生成这类指令。

 

在使用while循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更高效。如果循环对方向不敏感,即循环变量只是控制循环的次数,循环体和循环变量无关,可以由大向小循环。但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界。当然你可以通过对i做加减运算来纠正,但是这样加速的作用就没有了。

 

1.2    减小运算强度

数据的位是可以操作的最小数据单位,一般的位操作是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。

 

1.2.1    位操作实现求余运算

A求余运算

B 位操作

a=a%8;

a &= 7;

 

位操作只需一个指令周期即可完成,而大部分的C编译器的“%”运算均是运用除法指令来完成,代码长、执行速度慢。只要是求2^n的余数,均可使用位操作的方法来代替。但其可读性不好,所实现的功能不直观,若在循环内部进行大量此类操作时,位操作的优势将体现出来。

 

1.2.2    用移位实现乘除法运算

通常如果需要乘以或除以2^n,都可以用移位的方法代替。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果。

 

A 乘除法

#define DD_WORD2BYTE(word)                  ((word) * 4)

#define DD_BYTE2WORD(byte)                  (((byte) + 3) / 4)

a=a*9

B

移位

#define DD_WORD2BYTE(word)                  ((word) << 2)

#define DD_BYTE2WORD(byte)                  (((byte) + 3) >> 2)

a=(a<<3)+a

 

 

1.2.3    将循环体内的乘法运算改成循环自加运算

 

A 乘法

B 循环自加

for (i = 0; i < MAX; i++)

{

h = 14 * i;

}

h = 0;

for (i = 0; i < MAX; i++)

{

h += 14;

}

 

在循环过程中,将加法变成了减法,提高了效率。

 

1.3    减少不变计算

1.3.1    循环内部避免恒定式

对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式计算、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,操作完毕后保存在局部变量中,提高后续的访问效率。

 

A 内部

B 外部

for (i = 0; i < LEN; i++)

{

array[i] = a/b + 4;

}

u32Temp = a/b + 4;

for (i = 0; i < LEN; i++)

{

array[i] = u32Temp;

}

 

每次给array[i]赋固定的值“a/b + 4”,在A中需要每次计算其值,而在B中减少了LEN次此类计算。

 

A 内部

B 外部

for (i = 0; i < num * 4 + 2; i++)

{

array[i] = 0;

}

maxi = num * 4 + 2;

for (i = 0; i < maxi; i++)

{

array[i] = 0;

}

 

在循环内部,没有对num进行任何处理,其相对固定,因为在循环的过程中,“num * 4 + 2”不变。在A 内部”,每次计算结束条件时都需要计算,而“B 外部”中减少了“num * 4 + 2”次计算,减少了执行时间。

 

A 内部

B 外部

pu8Temp = pu8FileName;

u32Loop = 0;

while (*(pu8FileName + u32Loop) != '/0')

{

    if ('/' == *(pu8FileName + u32Loop))

    {

        pu8Temp = pu8FileName + u32Loop + 1;

    }

 

    u32Loop++;

}

pu8Temp = pu8FileName;

while (*pu8FileName != '/0')

{

    if ('/' == *pu8FileName)

    {

        pu8Temp = pu8FileName +  1;

    }

 

    pu8FileName++;

 

} /* (*pu8FileName != 0) */

 

这种情况比较复杂,尽管看上去表达式“pu8FileName + u32Loop”在每次循环时变化,但单次循环时其是不变的。由于u32Loop在循环内部变动,不管什么优化级别,都需要计算“pu8FileName + u32Loop”多次。在B中,将“pu8FileName + u32Loop”替换为一个表达式pu8FileName,减少了循环内部多次计算,但此时最初的pu8FileName改变了。

 

A 内部

B 外部

for (u32Loop = 0; u32FilelenMax > u32Loop; u32Loop++)

{

    pstruAlarmMsg->au8FileName[u32Loop] = *(pu8Temp + u32Loop);

}

pstruAlarmMsg->au8FileName[u32Loop] = '/0';

pu8AlarmMsgFileName = pstruAlarmMsg->au8FileName;

for (u32Loop = 0; u32FilelenMax > u32Loop; u32Loop++)

{

    *pu8AlarmMsgFileName++ = *pu8Temp++;

}

*pu8AlarmMsgFileName = '/0';

 

A的循环内部,每次都需要通过pstruAlarmMsg获取au8FileName首地址,其值在每次循环时是固定不变的,再加上可变的偏移量u32Loop,才能获得待写值;

B中,首先获得pu8AlarmMsgFileName指针,在循环内部无需再计数au8FileName的地址,同时多数处理器可以在*pu8AlarmMsgFileName 赋值的同时执行pu8AlarmMsgFileName++,即集成为了一个指令,减少了指令数,缩短了执行时间。

 

1.3.2    避免结构体深度访问

指针变量灵活多变,但通过指针对结构体进行深度访问时可能降低效率。凡是在同一模块内部对一个结构体的二级元素执行了多次访问,就有必要建立中间变量了,看下面的例子:

inline void DD_UPDATE_TS_ISR_GAP_CNT(STRU_TS_ISR_TIME_STATS *pstruTsIsrTime)

{

(pstruTsIsrTime)->u32IsrMinGapCnt =

(((pstruTsIsrTime)->struIsrTime.u32IsrCurGapTime) == ((pstruTsIsrTime)->struIsrTime.u32IsrMinGapTime)) ? ((pstruTsIsrTime)->struIsrTime.u32IsrTotalCnt - 1) : ((pstruTsIsrTime)->u32IsrMinGapCnt);

}

对于原有的二级元素访问,增添临时变量STRU_ISR_TIME_STATS *struIsrTime,将后续的二级访问改为一级访问了,减少了部分计算。

inline void DD_UPDATE_TS_ISR_GAP_CNT(STRU_TS_ISR_TIME_STATS *pstruTsIsrTime)

{

    STRU_ISR_TIME_STATS *struIsrTime = &(pstruTsIsrTime->struIsrTime);

    pstruTsIsrTime->u32IsrMinGapCnt = ((struIsrTime->u32IsrCurGapTime) == (struIsrTime->u32IsrMinGapTime)) ? (struIsrTime->u32IsrTotalCnt - 1) : (pstruTsIsrTime->u32IsrMinGapCnt);

}

 

当在循环内部使用二级访问时,这种影响更明显。

A 二级元素

for (u32Loop = 0; u32FilelenMax > u32Loop; u32Loop++)

{

    pstruAlarmLog->Msg->au8FileName[u32Loop] = pstruSrcAlarmLog->Msg->au8FileName[u32Loop];

}

pstruAlarmLog->Msg->au8FileName[u32Loop] = '/0';

B 纯指针

pu8SrcFileName  = pstruSrcAlarmLog->Msg->au8FileName;

pu8DstFileName  = pstruAlarmLog->Msg->au8FileName;

 

for (u32Loop = 0; u32FilelenMax > u32Loop; u32Loop++)

{

    * pu8DstFileName++ = * pu8SrcFileName ++;

}

* pu8DstFileName = '/0';

 

将二级元素数组的访问改为纯指针pu8DstFileName的访问,在避免结构体二级访问的同时还避免了计算数组元素的地址,大大提高了访问效率。

 

1.4    减少存储访问指令周期和个数

嵌入式处理器多采用哈佛体系结构,其典型特点是程序存储器和数据存储器访问时序分开,但统一编址,并有各自的寻址机构和寻址方式。通常有四种存储空间:

a)       片内ROM

b)       片外ROM

c)       片内RAM

d)       片外RAM

无论片内还是片外,本质上都是存储器,都可以用来存储程序和数据;如为了减少RAM存储空间,可以将只读数据放在ROM中;为了提高程序的运行速度,可以将代码放在RAM中运行。从存储访问速度看,片内RAM > 片外RAM > 片内ROM > 片外ROM

 

在定位变量时,经常访问的数据对象应放在内部RAM中,因为访问内部数据存储器要比访问外部数据存储器快得多。而对于内部RAM中的数据,编译器可能做进一步的优化,将其放在寄存器中,访问速度进一步提高。尽管指令个数未变,但指令的执行时间缩短了。

 

因此,当访问片外RAM或者Flash中的数据时,当需要多次读取或修改时,应遵循“读――改――写”模式,即首先读取片外RAM或者Flash中的数据,将其保存在片内RAM中,针对本地变量进行计算,计算完毕后再写回到片外RAM或者Flash中;而不是每修改一次就进行回写操作。当在循环内部访问片外RAM或者Flash中的数据时,尤其要注意。

pu32temp为指向flash的指针。

 

A  Flash

B  RAM

for (u32Loop = 0; MAX > u32Loop; u32Loop++)

{

    *pu32temp +=  *array++;

}

 

u32temp= *pu32temp;

for (u32Loop = 0; MAX > u32Loop; u32Loop++)

{

    u32temp +=  *array++;

}

*pu32temp = u32temp;

 

A中,每次访问pu32temp时都是采用flash的访问时序,而B中,首先“u32temp = *pu32temp;”获取初值,本地计算“u32temp +=  *array++”,最后回写“*pu32temp = u32temp;”,尽管多了两条指令,但大量缩短了指令执行时间。

 

同时有效的根据外扩存储器位数定义数据类型,可以减少访问时间;根据处理器字长定义数据类型时,当数据类型长度大于处理器字长时,需要额外的指令才能执行有关访问。

 

在进行大量数据拷贝或者初始化时,通常可以调用库函数memcopy和memset,但这些函数为了适应任意类型的数据拷贝或者初始化,其接受void*指针,在内部转换为u8 *,按照处理器最小单元字节流进行拷贝,一个u32类型数据需要进行sizeof(u32)次字节拷贝。DSP上经常会有大量的数据拷贝,若能利用和处理器字长匹配的存储指令特性,可以大大缩短指令数。

 

需要将一个STRU_DD_ALARM_MSG_LOG类型结构体变量从pu32Src地址处拷贝到pu32Dst地址处。

/* 驱动告警消息记录结构 */

typedef struct tag_STRU_DD_ALARM_MSG_LOG

{

u32                              u32AlarmFlag;       /* 告警标志    */

u8                      u8AlarmLevel;       /* 告警级别    */

u8                      au8Pad[3];             /* 填充        */

u16                     u16ErrId;                /* 告警编号    */

u16                     u16LineNum;        /* 告警行号         */

u8                      au8FileName[16];          /* 告警文件名       */

} STRU_DD_ALARM_MSG_LOG;

 

A

逐个赋值

STRU_DD_ALARM_MSG_LOG *pstru =  (STRU_DD_ALARM_MSG_LOG *) pu32Dst;

Pstru-> u32AlarmFlag = pstruCurAlarmMsgLog-> u32AlarmFlag;

…..

…..

B

按字拷贝

pu32Dst = C_DD_ALARM_LOG_DPRAM_BASE + g_u32DDAlarmCnt * C_DD_ALARM_LOG_ITEM_LEN_IN_WORD;

pu32Src = (u32 *)((u32) pstruCurAlarmMsgLog);

 

for (u32Loop = 0; C_DD_ALARM_LOG_ITEM_LEN_IN_WORD > u32Loop; u32Loop++)

{

    *pu32Dst++ = *pu32Src++;

 

} /* End */

 

传统的方法是根据结构体成员依次赋值,当结构体成员较多或者类型较小时,这种拷贝性能较低。采用memcopy的性能一样,只是编程稍微简单一点而已。而B充分利用和处理器字长匹配的字拷贝指令,大大减少了运行时间。但要注意,此时pu32Dst为指向u32类型的指针,因此待拷贝的数据必须位于u32对齐边界上,同时数据总字节数应是sizeof(u32)的倍数。

 

1.5    查表

在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些既消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区,但这需要预先计算出表的所有项。如果表很大,则直接生成所需的表比较困难,此时可以在启动时的初始化函数中先计算,然后在数据存储器中生成所需的表,以后在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。在RAM中初始化表的好处在于可以减小代码区,同时RAM的访问效率通常都比ROM高。

 

假设只计算factorial数列的前N项,且N不太大,当factorial调用很频繁时,查表的效率将显著提高。

A

B

long factorial(u32 i)

{

if (i == 0)

return 1;

else

return i * factorial(i - 1);

}

static long factorial_table[] =

{1, 1, 2, 6, 24, 120, 720 /* etc */};

long factorial(u32 i)

{

return factorial_table[i];

}

 

 

1.6    使用自加、自减指令

通常使用自加、自减指令和复合赋值表达式都能够生成高质量的程序代码,编译器通常都能够生成inc和dec之类的指令,其对内存字的增、减量操作不必明显地使用取内存和写内存的指令;而使用a=a+1或a=a-1之类的指令,有很多C编译器都会生成二到三个字节的指令。

 

比如下面这条语句:

x=x+1;

模仿大多数微机汇编语言为例, 产生的代码类似于:

move A,x ; 把x 从内存取出存入累加器A

add A,1 ; 累加器A 加1

store x ; 把新值存回x

如果使用增量操作符,生成的代码如下:

inc x ; x 加1

显然,不用取指令和存指令,增、减量操作执行的速度加快,同时长度也缩短了。

 

在许多种情况下,可以用指针运算代替数组索引,这样做常常能产生又快又短的代码。与数组索引相比,指针一般能使代码速度更快,占用空间更少。

 

下面的代码的作用是相同的,但是效率不一样。

  

A 数组索引

static void DD_InitTxFunc(void)

{

    u32                     u32Loop;

 

    for (u32Loop = 0; C_DD_MODULE_ID_MAX > u32Loop; u32Loop++)

    {

        g_apfDDTxFunc[u32Loop] = C_SYS_NULL;

    }

    return;

} /* static void DD_InitTxFunc(void) */

B指针运算

static void DD_InitTxFunc(void)

{

    u32                     u32Loop;

    PFUNC_DD_TX  *pfuncDdTx = g_apfDDTxFunc;

   

    for(u32Loop = 0; u32Loop < C_DD_MODULE_ID_MAX; u32Loop++ )

    {

        *pfuncDdTx++ = C_SYS_NULL;

    }

    return;

} /* static void DD_InitTxFunc(void) */

 

 

指针方法的优点是,获得pfuncDdTx 初值后,每次循环中,只需对pfuncDdTx增量操作,但由于多数处理器可以将赋值操作和增加pfuncDdTx集成在一个指令内,从而减少了指令个数,同时缩短了执行时间。在数组索引方法中,每次循环中都必须进行基于g_apfDDTxFunc和偏移量u32Loop获得待写地址,然后才能执行赋值操作。

 

1.7    根据频率进行case 排序

switch 语句是一个普通的编程技术,编译器会产生if-else-if 的嵌套代码,并按照顺序进行比较,发现匹配时,就跳转到满足条件的语句执行。使用时需要注意,每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,就把宝贵的处理器时间耗尽。为了提高速度,设法把具体的情况按照它们发生的相对频率排序。即把最可能发生的情况放在第一位,最不可能的情况放在最后。

 

1.8    函数指针表替代switchcase

如果switch 中每一种情况下都有很多的工作要做,那么把整个switch 语句用一个指向函数指针的表来替换会更加有效,比如下面的switch 语句,有三种情况:

enum MsgType{Msg1, Msg2, Msg3 }

switch (ReceiveMessage() {

  case Msg1:

 . .

  case Msg2:

.

  case Msg3:

.

 }

为了提高执行速度,用下面这段代码来替换这个上面的switch 语句。

/*准备工作*/

u32 handleMsg1(void); u32 handleMsg2(void); u32 handleMsg3(void);

/*创建一个函数指针数组,以便查表*/

u32 (*MsgFunction[])() = { handleMsg1, handleMsg2,handleMsg3};

/*用下面这行更有效的代码来替换switch 语句*/

status = MsgFunction[ReceiveMessage()]();

 

放弃了传统的switch-case结构,而是根据数组下标以统一的形式调用对应的函数,好处在于:

a)       从汇编角度的执行效率看没有switch方式的依次判断,执行流程比switch的平均方式要短;其只需要查找发送函数指针表,判断函数是否可用,即可跳转到对应的地址;

b)       从编译后的代码量来看,数组方式更小,但其占用了多余的内存区存放指针数组;

c)       从移植的角度看,无需对发送函数进行更改,只需更改对应的指针数组即可;

d)       从软件结构上看,程序的逻辑结构清晰直观,对外提供了统一的消息处理接口。

 

因为采用了统一的函数调用形式,其限制在于:

a)       各个函数原型必须统一,即参数类型和返回值类型一致,以便利用函数指针。通常可以利用u32 arg来传递任意的数据类型,以提供统一的接口,具体含义由函数根据自身功能解析;

b)       必须确保函数指针表正确初始化。

c)       由于靠下标来引用函数指针,需要静态分配指针标,就要求引用指针的参数尽量连续,以保证指针表尽量小。最好的情况是参数从0开始且连续。

 

DSP的驱动程序中多处使用了类似的指针表,如:

typedef SYS_STATUS (*PFUNC_DD_IOCTL)(u32 u32Arg);

PFUNC_DD_IOCTL              g_apfDDIoctlFunc[C_DD_IOCMD_CPP_NUM];

 

typedef SYS_STATUS (*PFUNC_DD_TX)(u32 u32SrcId, u32 u32DstId, u16 u16LinkId, u32 u32Len, void *pvoidData);

PFUNC_DD_TX                 g_apfDDTxFunc[C_DD_MODULE_TOTAL_NUM];

 

上述方式很好的实现了软件功能的分层,给上层提供了统一的调用接口。