看了一本嵌入式系统开发的书,里面讲到
“
高效的
C
编程
”
,有些不太理解。可能是平台不同,优化方法也不同吧。另外,在网上找到了同样的
C
编译优化,对比看一下:
C
语言编程优化篇
(
这个不知道讲的什么平台
)
1
、选择合适的算法和数据结构
应该熟悉算法语言
,
知道各种算法的优缺点
,
具体资料请参见相应的参考资料
,
有很多计算机书籍上都有介绍
.
将比较慢的顺序查找法用较快的二分查找或乱序查找法代替
,
插入排序或冒泡排序法用快速排序、合并排序或根排序代替
,
都可以大大提高程序执行的效率
..
选择一种合适的数据结构也很重要
,
比如你在一堆随机存放的数中使用了大量的插入和删除指令
,
那使用链表要快得多
.
数组与指针语句具有十分密切的关系
,
一般来说
,
指针比较灵活简洁
,
而数组则比较直观
,
容易理解
.
对于大部分的编译器
,
使用指针比使用数组生成的代码更短
,
执行效率更高
.
但是在
Keil
中则相反
,
使用数组比使用的指针生成的代码更短
.
但
SOC
平台上说:
=====================================================
12.
使用数组而不是指针,考虑透过指针存取数组的程序代码:
for (i=0; i<100; i++)
*p++ = ...
在每次循环中,
*p
被赋值。这种对指针对象的赋值会阻碍最佳化。某些情况下,指针指向它自己,那么这种赋值就会修改指针本身的值,这就会强迫编译器每次循环都重新加载该指针。还有,编译器不能确定这个指针不会被循环体以外所使用,所以每次循环外都要依据增量的数值更新该指针。因此,最好使用下面的程序代码:
for (i=0; i<100; i++)
p[i] = ...
=================================================================
2
、使用尽量小的数据类型
能够使用字符型
(char)
定义的变量
,
就不要使用整型
(int)
变量来定义
;
能够使用整型变量定义的变量就不要用长整型
(long int),
能不使用浮点型
(float)
变量就不要使用浮点型变量
.
当然
,
在定义变量后不要超过变量的作用范围
,
如果超过变量的范围赋值
,C
编译器并不报错
,
但程序运行结果却错了
,
而且这样的错误很难发现
.
在
ICCAVR
中
,
可以在
Options
中设定使用
printf
参数
,
尽量使用基本型参数
(%c
、
%d
、
%x
、
%X
、
%u
和
%s
格式说明符
),
少用长整型参数
(%ld
、
%lu
、
%lx
和
%lX
格式说明符
),
至于浮点型的参数
(%f)
则尽量不要使用
,
其它
C
编译器也一样
.
在其它条件不变的情况下
,
使用
%f
参数
,
会使生成的代码的数量增加很多
,
执行速度降低
.
但
ARM
平台上说:
================================================
6.
使用正确的数据类型
C
程序设计师对于数据类型一般都会有他们习惯上的假设,但是编译器却需要很谨慎地对待这些假设。例如,在几乎所有现代的计算机架构上,一个
unsigned char
使用
8
位表示从
0
到
255
。一个
C
程序会假设对值为
255
的
unsigned char
加
1
会使其变为
0
。而实际上,现代
32
位处理器不会执行上述的
8
位加法,而是进行
32
位数值加法。因此,如果一个
unsigned char
的本地变量进行加法,编译器必须使用多条指令进行运算以保证加法后的符号扩展。因此,针对各种变量尤其是循环索引的变量,应尽量多的在可以的地方使用
int
型变量。
另外,许多嵌入式处理器有
16
位乘法指令,而缺少
32
位乘法指令。在这种情况下,
32
位乘法将被仿效执行,一般情况下都是很慢的。如果数据被执行乘法作业并且运算结果不会超过
16
位的精密度,那么就使用
short
或者
unsigned short
变量。
==============================================================
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
方的余数
,
均可使用位操作的方法来代替
.
ARM
书上的取余运算:
将
offset = (offset + increment) % buffersize; //50 cycles
替换成:
offset += increment; // cycles if increment < buffer_size
while (offset >= buffersize)
{
offset -= buffer_size
}
(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
ARM
书上说:
把除法改变成乘法,如果一定要用除法,让除数为无符号数。
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.
使用宏定义
在
C
语言中
,
宏是产生内嵌代码的唯一方法
.
对于嵌入式系统而言
,
为了能达到性能要求
,
宏是一种很好的代替函数的方法
.
但不要给宏定义传入有副作用的参数
.
8.
使用寄存器变量
当对一个变量频繁被读写时
,
需要反复访问内存
,
从而花费大量的存取时间
.
为此
,C
语言提供了一种变量
,
即寄存器变量
.
这种变量存放在
CPU
的寄存器中
,
使用时
,
不需要访问内存
,
而直接从寄存器中读写
,
从而提高效率
.
寄存器变量的说明符是
register.
对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄存器变量
,
而循环计数是应用寄存器变量的最好候选者
.
(1)
只有局部自动变量和形参才可以定义为寄存器变量
.
因为寄存器变量属于动态存储方式
,
凡需要采用静态存储方式的量都不能定义为寄存器变量
,
包括
:
模块间全局变量、模块内全局变量、局部
static
变量
;
(2) register
是一个建议型关键字
,
意指程序建议该变量放在寄存器中
,
但最终该变量可能因为条件不满足并未成为寄存器变量
,
而是被放在了存储器中
,
但编译器中并不报错
(
在
C++
语言中有另一个建议型关键字
:inline).
下面是一个采用寄存器变量的例子
:
/*
求
1+2+3+….+n
的值
*/
WORD Addition(BYTE n)
{
register i,s=0;
for(i=1;i<=n;i++)
return s;
}
本程序循环
n
次
,i
和
s
都被频繁使用
,
因此可定义为寄存器变量
.
9.
内嵌汇编
程序中对时间要求苛刻的部分可以用内嵌汇编来重写
,
以带来速度上的显著提高
.
但是
,
开发和测试汇编代码是一件辛苦的工作
,
它将花费更长的时间
,
因而要慎重选择要用汇编的部分
.
在程序中
,
存在一个
80-20
原则
,
即
20%
的程序消耗了
80%
的运行时间
,
因而我们要改进效率
,
最主要是考虑改进那
20%
的代码
.
嵌入式
C
程序中主要使用在线汇编
,
即在
C
程序中直接插入
_asm{ }
内嵌汇编语句
:
/*
把两个输入参数的值相加
,
结果存放到另外一个全局变量中
*/
int result;
void Add(long a, long *b)
{
_asm
{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}
10.
利用硬件特性
首先要明白
CPU
对各种存储器的访问速度
,
基本上是
:
CPU
内部
RAM >
外部同步
RAM >
外部异步
RAM >FLASH/ROM
对于程序代码
,
已经被烧录在
FLASH
或
ROM
中
,
我们可以让
CPU
直接从其中读取代码执行
,
但通常这不是一个好办法
,
我们最好在系统启动后将
FLASH
或
ROM
中的目标代码拷贝入
RAM
中后再执行以提高取指令速度
;
对于
UART
等设备
,
其内部有一定容量的接收
BUFFER,
我们应尽量在
BUFFER
被占满后再向
CPU
提出中断
.
例如计算机终端在向目标机通过
RS-232
传递数据时
,
不宜设置
UART
只接收到一个
BYTE
就向
CPU
提中断
,
从而无谓浪费中断处理时间
;
如果对某设备能采取
DMA
方式读取
,
就采用
DMA
读取
,DMA
读取方式在读取目标中包含的存储信息较大时效率较高
,
其数据传输的基本单位是块
,
而所传输的数据是从设备直接送入内存的
(
或者相反
).DMA
方式较之中断驱动方式
,
减少了
CPU
对外设的干预
,
进一步提高了
CPU
与外设的并行操作程度
.
11.
活用位操作
C
语言位运算除了可以提高运算效率外
,
在嵌入式系统的编程中
,
它的另一个最典型的应用
,
而且十分广泛地正在被使用着的是位间的与
(&)
、或
(|)
、非
(~)
操作
,
这跟嵌入式系统的编程特点有很大关系
.
我们通常要对硬件寄存器进行位设置
,
譬如
,
我们通过将
AM186ER
型
80186
处理器的中断屏蔽控制寄存器的第低
6
位设置为
0(
开中断
2),
最通用的做法是
:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);
而将该位设置为
1
的做法是
:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp | INT_I2_MASK);
判断该位是否为
1
的做法是
:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
if(wTemp &INT_I2_MASK)
{
… /*
该位为
1 */
}
12
、其它
比如使用在线汇编及将字符串和一些常量保存在程序存储器中
,
均有利于优化
.
上述方法在嵌入式系统的编程中是非常常见的
,
我们需要牢固掌握
.
ARM
书上还说:
1
、展开重要的循环,来减小循环开销。
如:
int checksum(int *data, unsigned int N)
{
int sum = 0;
do
{
sum+= *(data++);
}while(--N != 0);
return sum;
}
展开成
(
假定
N
是
4
的倍数
)
int checksum(int *data, unsigned int N)
{
int sum = 0;
do
{
sum+= *(data++);
sum+= *(data++);
sum+= *(data++);
sum+= *(data++);
N-=4;
}while(N != 0);
return sum;
}
2
、尽可能把函数参数限制在
4
个以内,以使参数都放在寄存器中。
3
、按元素尺寸由小到大排列建立结构体,特别是在
thumb
模式下编译。以使访问结构体靠后的元素时,使用更小的偏移。
避免使用大结构体,可以用层次化的小结构体来代替。
为了提高移植性,人工对
API
的结构体增加填充位。
在
API
的结构体中,谨慎使用枚举类型,一个枚举类型的大小是编译器相关的。
4
、不要使用位域,用掩码和逻辑操作来代替。
5
、尽量不使用边界不对齐的数据。
SOC
平台上还说:
9.
传递变量时使用数值而不是指针或者全局变量
传递大结构的数据时才使用指针。每个透过数值被传递的结构都应该在函数调用入口处被完全拷贝储存过。
11.
用
const
声明指针参数
如果函数体内不会修改到指针指向的对象,就要用
const
声明指针参数,这样可以让编译器避免不必要的反面假设
14.
避免编写参数数量可变的函数
如果一定要这么做,使用
ANSI
标准方法:
stdarg.h.
。使用数据表替代
if-then-else
或者
switch
分支处理。如考虑以下程序代码:
typedef enum { BLUE, GREEN, RED, NCOLORS } COLOR;
替代
switch ( c ) {
case CASE0: x = 5; break;
case CASE1: x = 10; break;
case CASE2: x = 1; break;
}
使用
static int Mapping[NCOLORS] = { 5, 10, 1 };
...
x = Mapping[c];
总结
在性能优化方面永远注意
80-20
准备
,
不要优化程序中开销不大的那
80%,
这是劳而无功的
.
宏定义是
C
语言中实现类似函数功能而又不具函数调用和返回开销的较好方法
,
但宏在本质上不是函数
,
因而要防止宏展开后出现不可预料的结果
,
对宏的定义和使用要慎而处之
.
很遗憾
,
标准
C
至今没有包括
C++
中
inline
函数的功能
,inline
函数兼具无调用开销和安全的优点
.
使用寄存器变量、内嵌汇编和活用位操作也是提高程序效率的有效方法
.
除了编程上的技巧外
,
为提高系统的运行效率
,
我们通常也需要最大可能地利用各种硬件设备自身的特点来减小其运转开销
,
例如减小中断次数、利用
DMA
传输方式等
发表于
2008-12-25 17:55 游子
阅读
(104) 评论(1) 编辑 收藏引用
所属分类
: 软件
、
原创技术