优化

1、采用短变量

一个提高代码效率的最基本的方式就是减小变量的长度。使用 C 编程时,我们都习惯于对循环控制变量使用 int 类型,这对 8 位的单片机来说是一种极大的浪费,你应该仔细考虑你所声明的变量值可能的范围,然后选择合适的变量类型,很明显,经常使用的变量应该是unsigned char,只占用一个字节。

2、使用无符号类型

为什么要使用无符号类型呢?原因是8051不支持符号运算,程序中也不要使用含有带符号变量的外部代码,除了根据变量长度来选择变量类型外,你还要考虑是否变量是否会用于负数的场合。如果你的程序中可以不需要负数那么把变量都定义成无符号类型的。

3、避免使用浮点指针

在 8 位操作系统上使用 32 位浮点数是得不偿失的。你可以这样做,但会浪费大量的时间,所以当你要在系统中使用浮点数的时候,你要问问自己这是否一定需要,可以通过提高数值数量级和使用整型运算来消除浮点指针,处理ints和longs比处理doubles和floats要方便得多,你的代码执行起来会更快,也不用连接处理浮点指针的模块。如果你一定要,采用浮点指针的话,你应该采用西门子 80517 和达拉斯半导体公司的 80320 这些已经对数,处理进行过优化的单片机。如果你不得不在你的代码中加入浮点指针,那么你的代码长度会增加程序执行速度也会比较慢。如果浮点指针运算能被中断的话,你必须确保要么中断中不会使用浮点指针运算,要么在中断程序前使用 fpsave 指令把中断指针推入堆栈,在中断程序执行后使用 fprestore 指令把指针恢复,还有一种方法是,当你要使用像 sin()这样的浮点运算程序时,禁止使用中断,在运算程序执行完之后再使能它。

4、使用位变量

对于某些标志位应使用位变量而不是 unsigned char,这将节省你的内存,你不用多浪费7位存储区,而且位变量在RAM中访问他们只需要一个处理周期。

5、用局部变量代替全局变量

把变量定义成局部变量比全局变量更有效率,编译器为局部变量在内部存储区中分配存储空间,而为全局变量在外部存储区中分配存储空间,这会降低你的访问速度,另一个避免使用全局变量的原因是你必须在你系统的处理过程中调节使用全局变量,因为在中断系统和多任务系统中,不止一个过程会使用全局变量。

6、为变量分配内部存储区

局部变量和全局变量可被定义在你想要的存储区中,根据先前的讨论,当你把经常使用的变量放在内部 RAM 中时,可使你的程序的速度得到提高,除此之外,你还缩短了你的代码,因为外部存储区寻址的指令相对要麻烦一些考虑到存储速度,按下面的顺序使用存储器DATA IDATA PDATA XDATA,当然你要记得留出足够的堆栈空间。

7、使用特定指针

当你在程序中使用指针时,你应指定指针的类型确定它们指向哪个区域如 XDATA 或CODE 区,这样你的代码会更加紧凑,因为编译器不必去确定指针所指向的存储区,因为你已经进行了说明。

8、使用调令

对于一些简单的操作,如变量循环位移,编译器提供了一些调令供用户使用,许多调令直接对应着汇编指令,而另外一些比较复杂并兼容 ANSI 所有这些调令都是再入函数,你可在任何地方安全的调用他们和单字节循环位移指令 RL A 和 RR A 相对应的调令是_crol_ 循环左移 和_cror_(循环右移)。如果你想对 int 或 long 类型的变量进行循环位移,调令将更加复杂而且执行的时间会更长 对于 int 类型调令为_irol_,_iror_ ,对于 long 类型调令为_lrol_,_lror_。在 C 中也提供了像汇编中 JBC 指令那样的调令_testbit_ ,如果参数位置位他将返回1,否则将返回 0 这条调令在检查标志位时十分有用,而且使 C 的代码更具有可读性调令将直接转换成 JBC 指令。

#include <instrins.h>
void serial_intr(void) interrupt 4 {
if (!_testbit_(TI)) { // 是否是发送中断
P0=1; // 翻转 P0.0
_nop_(); // 等待一个指令周期
P0=0;
...
}
if (!_testbit_(RI)) {
test=_cror_(SBUF, 1); // 将SBUF中的数据循环
// 右移一位
...
}
}

9、使用宏替代函数

对于小段代码,像使能某些电路或从锁存器中读取数据,你可通过使用宏来替代函数使得程序有更好的可读性你可把代码定义在宏中,这样看上去更像函数。编译器在碰到宏时,按照事先定义的代码去替代宏,宏的名字应能够描述宏的操作,当需要改变宏时,你只要修该宏定义处。

#define led_on() {\
led_state=LED_ON; \
XBYTE[LED_CNTRL] = 0x01;}
#define led_off() {\
led_state=LED_OFF; \
XBYTE[LED_CNTRL] = 0x00;}
#define checkvalue(val) \
( (val < MINVAL || val > MAXVAL) ? 0 : 1 )

宏能够使得访问多层结构和数组更加容易,可以用宏来替代程序中经常使用的复杂语句以减少你打字的工作量且有更好的可读性和可维护性。

10、存储器模式

C51提供了 3 种存储器模式来存储变量、过程参数和分配再入函数堆栈。你应该尽量使用小存储器模式,很少应用系统需要使用其它两种模式,像有大的再入函数堆栈系统那样。一般来说如果系统所需要的内存数小于内部RAM 数时,都应以小存储模式进行编译。在这种模式下 DATA 段是所有内部变量和全局变量的默认存储段,所有参数传递都发生在DATA 段中,如果有函数被声明为再入函数,编译器会在内部 RAM 中为他们分配空间,这种模式的优势就是数据的存取速度很快,但只有120个字节的存储空间供你使用,总共有128个字节,但至少有8个字节被寄存器组使用,你还要为程序调用开辟足够的堆栈。如果你的系统有 256 字节或更少的外部 RAM 你可以使用压缩存储模式。这样一来,如果不加说明,变量将被分配在 PDATA 段中,这种模式将扩充你能够使用的 RAM 数量,对XDATA 段以外的数据存储仍然是很快的,变量的参数传递将在内部 RAM 中进行,这样存储速度会比较快,对 PDATA 段的数据的寻址是通过 R0 和R1进行间接寻址,比使用 DPTR 要快一些在大存储模式中,所有变量的默认存储区是 XDATA 段 Keil C 尽量使用内部寄存器组进行参数传递,在寄存器组中可以传递参数的数量和和压缩存储模式一样,再入函数的模拟栈将在 XDATA中 对 XDATA 段数据的访问是最慢的,所以要仔细考虑变量应存储的位置使数据的存储速度得到优化。

11、混合存储模式

Keil 允许使用混合的存储模式,这点在大存储模式中是非常有用的。在大存储器模式下,有些过程对数据传递的速度要求很高。我就把过程定义在小存储模式寄存器中,这使得编译器为该过程的局部变量在内部 RAM中分配存储空间,并保证所有参数都通过内部 RAM进行传递。尽管采用混合模式后编译的代码长度不会有很大的改变,但这种努力是值得的就像能在大模式下把过程声明为小模式一样,你像能在小模式下把过程声明为压缩模或大模式,这一般使用在需要大量存储空间的过程上,这样过程中的局部变量将被存储在外部存储区中,你也可以通过过程中的变量声明,把变量分配在 XDATA 段中。

12、运行库

运行库中提供了很多短小精悍的函数,你可以很方便的使用他们,你自己很难写出更好的代码了。值得注意的是库中有些函数不是再入函数,如果在执行这些函数的时候被中断,而在中断程序中又调用了该函数,将得到意想不到的结果。而且这种错误很难找出来,最好禁止使用这些函数的中断。

本文转载自:博客园 - 诸葛惊云

围观 411

对程序进行优化,通常是指优化程序代码或程序执行速度。优化代码和优化速度实际上是一个予盾的统一,一般是优化了代码的尺寸,就会带来执行时间的增加,如果优化了程序的执行速度,通常会带来代码增加的副作用,很难鱼与熊掌兼得,只能在设计时掌握一个平衡点。

一、程序结构的优化

1、程序的书写结构

虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该尊循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。在书写程序时,特别是对于While、for、do…while、if…elst、switch…case 等语句或这些语句嵌套组合时,应采用“缩格”的书写形式,

2、标识符

程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:

count、number1、red、work 等。

3、程序结构

C 语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用C 语言设计 单片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可使整个应用系统程序结构清晰,便于调试和维护。于一个较大的应用程序,通常将整个程序按功能分成若干个模块,不同模块完成不同的功能。各个模块可以分别编写,甚至还可以由不同的程序员编写,一般单个模块完成的功能较为简单,设计和调试也相对容易一些。在C 语言中,一个函数就可以认为是一个模块。所谓程序模块化,不仅是要将整个程序划分成若干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保持模块的独立性,尽量少使用 全局变量等。对于一些常用的功能模块,还可以 封装为一个应用程序库,以便需要时可以直接调用。但是在使用模块化时,如果将模块分成太细太小,又会导致程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。

4、定义常数

在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。

5、减少判断语句

能够使用条件编译(ifdef)的地方就使用条件编译而不使用if 语句,有利于减少编译生成的代码的长度。

6、表达式

对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。

7、函数

对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保证它与原来定义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。如果果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数,在Keil 中的高级别优化就是这样的。如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报错,这样会增加排错的难度。

8、尽量少用全局变量,多用局部变量。因为全局变量是放在数据存储器中,定义一个全局变量,MCU 就少一个可以利用的数据存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配。而局部变量大多定位于MCU 内部的寄存器中,在绝大多数MCU 中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所的占用的寄存器和数据存储器在不同的模块中可以重复利用。

9、设定合适的编译程序选项

许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最合适的一种优化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,可能会影响程序的正确性,导致程序运行出错。因此应熟悉所使用的编译器,应知道哪些参数在优化时会受到影响,哪些参数不会受到影响。

在ICCAVR 中,有“Default”和“Enable Code Compression”两个优化选项。

在CodeVisionAVR 中,“Tiny”和“small”两种内存模式。

在IAR ==有7 种不同的内存模式选项。

在GCCAVR 中优化选项更多,一不小心更容易选到不恰当的选项。

二、代码的优化

1、选择合适的算法和数据结构

应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率。.选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。

数组与指针具有十分密码的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。但是在Keil 中则相反,使用数组比使用的指针生成的代码更短。

2、使用尽量小的数据类型

能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型 变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。在ICCAVR 中,可以在Options 中设定使用printf 参数,尽量使用基本型参数(%c、%d、%x、%X、%u 和%s 格式说明符),少用长整型参数(%ld、%lu、%lx 和%lX 格式说明符),至于浮点型的参数(%f)则尽量不要使用,其它C 编译器也一样。在其它条件不变的情况下,使用%f 参数,会使生成的代码的数量增加很多,执行速度降低。

3、使用自加、自减指令

通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1 之类的指令,有很多C 编译器都会生成二到三个字节的指令。在AVR 单片适用的ICCAVR、GCCAVR、IAR 等C 编译器以上几种书写方式生成的代码是一样的,也能够生成高质量的inc 和dec 之类的的代码。

4、减少运算的强度

可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下:

(1)、求余运算。

a=a%8;

可以改为:

a=a&7;

说明:位操作只需一个 指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n 方的余数,均可使用位操作的方法来代替。

(2)、平方运算

a=pow(a,2.0);

可以改为:

a=a*a;

说明:在有内置硬件乘法器的单片机中(如51 系列),乘法运算比求平方运算快得多,因为 浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR 单片机中,如ATMega163 中,乘法运算只需2 个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR 单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。

如果是求3 次方,如:

a=pow(a,3.0);

更改为:

a=a*a*a;

则效率的改善更明显。

(3)、用移位实现乘除法运算

a=a*4;

b=b/4;

可以改为:

a=a<<2;

b=b>>2;

说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR 中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:

a=a*9

可以改为:

a=(a<<3)+a

5、循环

(1)、循环语

对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init 的初始化程序中进行。

(2)、延时函数:

通常使用的延时函数均采用自加的形式:

  void delay (void)

  {

  unsigned int i;

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

  }

  将其改为自减延时函数:

  void delay (void)

  {

  unsigned int i;

  for (i=1000;i>0;i--);

  }

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

在使用while 循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3 个字母。

但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。

(3)while 循环和do…while 循环

用while 循环时有以下两种循环形式:

  unsigned int i;

  i=0;

  while (i<1000)

  {

  i++;

  //用户程序

  }

  或:

  unsigned int i;

  i=1000;

  do

  i--;

  //用户程序

  while (i>0);

在这两种循环中,使用do…while 循环编译后生成的代码的长度短于while 循环。

6、查表

在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,后以在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。

7、其它

比如使用在线汇编及将字符串和一些常量保存在程序存储器中,均有利于优化

围观 543
订阅 RSS - 优化