1.关于数据类型的选择
在单片机C语言编程中,能够使用char定义的变量,就不要使用int来定义;能够使用int定义的变量就不要用long int,能不使用float就不要使用float。
当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,很多C编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。
个人习惯在一些不涉及字符字符串的不涉及复杂数学运算的代码中,尽量统一用unsigned char ,这样在衔接一些通信协议的时候使用比较方便。
2、关于运算强度的选择
(1)、查表/运算
个人习惯,尽量避免在自己的主循环里进行运算工作,尽可能先计算好了,再到循环里查表。例1 以常见的阶乘函数为例
旧代码:
long factorial(int i)
{
if (i == 0)
return 1;
else
return i * factorial(i - 1);
}
新代码:
static long factorial_table[] = {1, 1, 2, 6, 24, 120, 720 ……};
long factorial(int i)
{
return factorial_table[i];
}
有时候表很大,就写一个init函数,在循环外临时生成表格,然后在循环中查表。这个在一些简单的数据转换代码中经常能用到。
(2)求余
a=a%8;
可以改为:
a=a&7;
位操作只需一个指令周期即可完成,%运算有的是调用子程序来完成,代码长,执行速度慢。通常,只要求是求2的n方的余数,都可使用位操作的方法来代替。
(3)平方
a=pow(a, 2.0);
可以改为:
a=a*a;
51系列一般都有内置硬件乘法器,乘法运算比求平方运算快得多。既使是在没有内置硬件乘法器的单片机中,乘法运算的子程序也比平方运算的子程序代码短,执行速度快。
如果是求3次方,如:
a=pow(a,3.0);
更改为:
a=a*a*a;
则效率的改善更明显。
(4)除法
a=a*4;
b=b/4;
可以改为:
a=a<<2;
b=b>>2;
如果需要乘以或除以2n,都可以用移位的方法代替。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。
实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:
a=a*9
可以改为(a=a*(8+1)):
a=(a<<3)+a
3、循环
(1)充分分解小的循环
要充分利用CPU的指令缓存,就要充分分解小的循环。特别是当循环体本身很小的时候,分解循环可以提高性能。
(2)提取公共部分
对于一些不需要循环变量参加运算的计算任务可以把它们放到循环外面。对于那些在循环中调用的函数,凡是没必要执行多次的操作通通提出来,放到一个init函数里,循环前调用。另外尽量减少喂食次数,没必要的话尽量不给它传参,需要循环变量的话让它自己建立一个静态循环变量自己累加,速度会快一点。
还有就是结构体访问,凡是在循环里对一个结构体的两个以上的元素执行了访问,就有必要建立中间变量了。
旧代码:
total = a->b->c[4]->aardvark + a->b->c[4]->baboon + a->b->c[4]->cheetah + a->b->c[4]->dog;
新代码:
struct animals * temp = a->b->c[4];
total = temp->aardvark + temp->baboon + temp->cheetah + temp->dog;
(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循环。
(4)循环嵌套
把相关循环放到一个循环里,也会加快速度。
旧代码:
for (i = 0; i < MAX; i++)
for (j = 0; j < MAX; j++)
a[i][j] = 0.0;
for (i = 0; i < MAX; i++)
a[i][i] = 1.0;
新代码:
for (i = 0; i < MAX; i++)
{
for (j = 0; j < MAX; j++)
a[i][j] = 0.0;
a[i][i] = 1.0;
}
(5)Switch
当switch用比较链的方式转化时,编译器会产生if-else-if的嵌套代码,并按照顺序进行比较,匹配时就跳转到满足条件的语句执行。所以可以对case的值依照发生的可能性进行排序,把最有可能的放在第一位,这样可以提高性能。
当switch语句中的case标号很多时,为了减少比较的次数,明智的做法是把大switch语句转为嵌套switch语句。把发生频率高的case 标号放在一个switch语句中,并且是嵌套switch语句的最外层,发生相对频率相对低的case标号放在另一个switch语句中。比如,下面的程序段把相对发生频率低的情况放在缺省的case标号内。
pMsg=ReceiveMessage();
switch (pMsg->type)
{
case FREQUENT_MSG1:
handleFrequentMsg();
break;
。。。。。。
default: //嵌套部分用来处理不经常发生的消息
switch (pMsg->type)
{
case INFREQUENT_MSG1:
handleInfrequentMsg1();
break;
。。。。。。
}
}
(6)关于循环的性能
要提升循环的性能,可以减少不随循环变化的代码段。
for( i 。。。)
{
if( CONSTANT0 )
{
DoWork0( i );// 假设这里不改变CONSTANT0的值
}
else
{
DoWork1( i );// 假设这里不改变CONSTANT0的值
}
}
推荐的代码:
if( CONSTANT0 )
{
for( i 。。。)
{
DoWork0( i );
}
}
else
{
for( i 。。。)
{
DoWork1( i );
}
}
如果已经知道if()的值,这样可以避免重复工作。虽然不好的代码中的分支可以简单地预测,但是由于推荐的代码在进入循环前分支已经确定,就可以减少对分支预测的依赖。
(7)无限循环
在编程中,我们常常需要用到无限循环,常用的两种方法是while (1)和for (;;)。如何选择?在编译的汇编过程可以看到,for (;;)指令少,不占用寄存器,而且没有判断、跳转,比while (1)好。
4、避免读写依赖
当数据保存到内存时存在读写依赖,即数据必须在正确写入后才能再次读取。在一段很长的又互相依赖的代码链中,避免读写依赖显得尤其重要。简单来说,引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。
不好的代码:
float x[VECLEN], y[VECLEN], z[VECLEN];
。。。。。。
for (unsigned int k = 1;k < VECLEN;k ++)
{
x[k] = x[k-1] + y[k];
}
for (k = 1;k <VECLEN;k++)
{
x[k] = z[k] * (y[k] - x[k-1]);
}
推荐的代码:
float x[VECLEN], y[VECLEN], z[VECLEN];
。。。。。。
float t(x[0]);
for (unsigned int k = 1;k < VECLEN;k ++)
{
t = t + y[k];
x[k] = t;
}
t = x[0];
for (k = 1;k <;VECLEN;k ++)
{
t = z[k] * (y[k] - t);
x[k] = t;
}
5、函数
(1)不定义不使用的返回值
函数定义并不知道函数返回值是否被使用,假如返回值从来不会被用到,应该使用void来明确声明函数不返回任何值。
(2)对函数的错误返回码要全面处理
一个函数能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。
示例:
char fileHead[128];
ReadFileHead(fileName, fileHead, sizeof(fileHead)); // Bad: 未检查返回值
DealWithFileHead(fileHead, sizeof(fileHead)); // fileHead 可能无效
正确写法:
char fileHead[128];
ret = ReadFileHead(fileName, fileHead, sizeof(fileHead));
if (ret != OK) { // Good: 确保 fileHead 被有效写入
return ERROR;
}
DealWithFileHead(fileHead, sizeof(fileHead)); // 处理文件头
(3)把本地函数声明为static
如果一个函数只在实现它的文件中被使用,把它声明为static以强制使用内部连接。否则,默认的情况下会把函数定义为外部连接。这样可能会影响某些编译器的优化——比如,自动内联。
(4)优先使用返回值而不是输出参数
使用返回值而不是输出参数,可以提高可读性,并且通常提供相同或更好的性能。
函数名为 GetXxx、FindXxx 或直接名词作函数名的函数,直接返回对应对象,可读性更好。
6、使用嵌套的if结构
在if结构中如果要判断的并列条件较多,最好将它们拆分成多个if结构,然后嵌套在一起,这样可以避免无谓的判断。
7、头文件循环依赖
简单的程序,尽量避免头文件循环依赖,指 a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h, 导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。
而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。
8、引用外部函数接口、变量
建议通过包含头文件的方式使用其他模块或文件提供的接口。
通过 extern 声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。
最大的危害是,这种隐式依赖,容易导致架构混乱。
不好的代码:
a.c 内容
extern int Foo(void); // Bad: 通过 extern 的方式引用外部函数
void Bar(void)
{
int i = Foo(); // 这里使用了外部接口 Foo
...
}
应该改为:
a.c 内容
#include "b.h" // Good: 通过包含头文件的方式使用其他.c提供的接口
void Bar(void)
{
int i = Foo();
...
}
b.h 内容
int Foo(void);
b.c内容
int Foo(void)
{
// Do something
}
9、goto
goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。使用时,也只允许跳转到本函数goto语句之后的语句。
goto语句通常用来实现函数单点返回。
同一个函数体内部存在大量相同的逻辑但又不方便封装成函数的情况下,譬如反复执行文件操作, 对文件操作失败以后的处理部分代码(譬如关闭文件句柄,释放动态申请的内存等等), 一般会放在该函数体的最后部分,在需要的地方就goto到那里,这样代码反而变得清晰简洁。实际也可以封装成函数或者封装成宏,但是这么做会让代码变得没那么直接明了。
示例:
// Good: 使用 goto 实现单点返回
int SomeInitFunc(void)
{
void *p1;
void *p2 = NULL;
void *p3 = NULL;
p1 = malloc(MEM_LEN);
if (p1 == NULL) {
goto EXIT;
}
p2 = malloc(MEM_LEN);
if (p2 == NULL) {
goto EXIT;
}
p3 = malloc(MEM_LEN);
if (p3 == NULL) {
goto EXIT;
}
DoSomething(p1, p2, p3);
return 0; // OK.
EXIT:
if (p3 != NULL) {
free(p3);
}
if (p2 != NULL) {
free(p2);
}
if (p1 != NULL) {
free(p1);
}
return -1; // Failed!
}
本文介绍了C语言编程中的一些优化策略,包括选择合适的数据类型,避免不必要的运算,优化循环结构,以及合理使用函数等。强调了在不影响功能的前提下,减少内存消耗和提高执行效率的重要性,例如使用位操作替代求余运算,分解小循环,以及避免读写依赖等。此外,还提到了函数设计的注意事项,如避免无用的返回值,全面处理错误返回码,以及使用静态函数提高性能。
744

被折叠的 条评论
为什么被折叠?



