标签:嵌入式
本文是《ARM嵌入式系统开发:软件设计与优化》一书部分学习笔记,实验均在IAR上完成。目标板芯片为M3内核。
一、 基本的C数据类型
局部变量类型
ARM处理器内部是32位寄存器和32位的数据处理操作。其结构体系是RISC load/store结构。换句话说,数据在使用之前必须将其从内部装载到内部寄存器,任何算术或者逻辑指“令都不能直接在存储器里进行操作。
8位或者16位数据在装载/存储ARM之前,先要扩展成32位。无符号数0作为扩展位,有符号数则按照符号位扩展。这就意味着装载int类型数据无需花费多余指令时间进行位扩展。同样,8位或16位数据必须放在寄存器的低8位或者低16位。而一个int或者更小类型的传送,存储时就不要花费额外的指令时间了。
示例:其中UXTB为无符号扩展一个半字到32位。
int checksum(int *data)
{
char i;
int sum = 0;
for(i=0;i<64;i++)
{
sum += data[i];
}
return sum;
}
反汇编
可以看到对于char类型变量i,编译器先将其用UXTB无符号扩展指令将其扩展到字在进行运算。将其改为int型。
反汇编
可以看到,编译器直接将i做运算,并无扩展指令。
函数参数类型
short add_v1(short a,short b)
{
return( a + (b >> 1));
}
对于编译器。输入值a,b和返回值都存放在32位的ARM寄存器中。编译器是否应该考虑这些32位数值在short类型的范围(-32768 ~+32768)之间呢?或者编译器是否应该通过对低16位数据进行符号位扩展,填充32位寄存器,强制把数值限制在上述范围之间呢。
反汇编
可以看到,不论是输入参数还是返回值,编译器都是将其扩展到32位进行运算,连运算都是以32位加法指令进行的。返回值也是扩展为32位返回。
Note:
在Thumb-2指令集中,有些操作既可以由16位指令完成,也可以由32位指令完成。例如,R0=R0+1这样的操作,16位的与32位的指令都提供了助记符为“ADD”的指令。在UAL下,汇编器能主动决定用哪个,也可以手工指定是用16位的还是32位的:
ADDS R0, #1 ;汇编器将为了节省空间而使用16位指令
ADDS.N R0, #1 ;指定使用16位指令(N=Narrow)
ADDS.W R0, #1 ;指定使用32位指令(W=Wide)
W(Wide)后缀指定32位指令。如果没有给出后缀,汇编器会先试着用16位指令以给代码瘦身,如果不行再使用32位指令。因此,使用“.N”其实是多此一举,不过汇编器可能仍然允许这样的语法。
再次重申,这是ARM公司汇编器的语法,其它汇编器的可能略有区别,但如果没有给出后缀,汇编器就总是会尽量选择更短的指令。其实在绝大多数情况下,应用程序是用C写的,C编译器也会尽可能地使用短指令。然而,当立即数超出一定范围时,或者32位指令能更好地适合某个操作,将使用32位指令。
总结
对于存放在寄存器中的局部变量,除了8位或16位的算术取模运算外,尽量不要使用char和short类型,需要使用有符号或者无符号int类型。除法运算时使用无符号数执行速度更快。
对于存放在主存储器中的数组和全局变量,在满足大小的前提下,应尽可能使用小尺寸数据类型,这样做可以节省空间。ARMv4体系结构可以有效的装载和存储所有数据宽度。
通过读取数组或者全局变量并赋值给不同类型的变量时,或者把局部变量写入不同类型的数组或者全局变量时,要进行显示(explict)数据类型转换,这种转换可使编译器可以明确快速处理把存储器中的数据宽度比较窄的数据类型扩展。
由于显示或者隐式的数据类型转换通常会有额外的指令开销,所以在表达式中应尽量避免使用。
对于函数参数和返回值应尽量避免使用char和short类型。即使参数范围较小,也应该使用int型,以防止编译器做不必要的转换。
二 循环结构
固定次数循环
int checksum(int *data)
{
int i;
int sum = 0;
for(i=0;i<64;i++)
{
sum += data[i];
}
return sum;
}
反汇编
这里用了3条指令来实现for循环结构:
int checksum_v1(int *data)
{
int i;
int sum = 0;
for(i=64;i!=0;i--)
{
sum += *(data++);
}
return sum;
}
反汇编
不固定次数循环
int checksum_v1(int *data,unsigned int n)
{
int sum = 0;
for(;n!=0;n--)
{
sum += *(data++);
}
return sum;
}
反汇编
在函数的入口处,编译器先检查N是否为0,由于数组通常不会为空,所以一般来书检查是没有必要的。下面的例子说明使用do-while循环会比for循环呈现更好的性能和代码密度。
int checksum_v3(int *data,unsigned int n)
{
int sum = 0;
do
{
sum += *(data++);
}while(--n!=0);
return sum;
}
反汇编
实际上,在IAR中,打开优化选项,选择low,则不固定次循环这两种情况产生的汇编代码时一样的。
循环展开
在循环中,每次循环需要在循环体外加2条指令:一条减法指令来减少循环计数值和一条条件分支指令。通常把这些指令称为循环开销。
可以通过展开循环体—-重复循环主体多次,并按同样比例减少循环次数来降低循环开销,如下所示。
int checksum_v3(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个指针指向同一个地址对象时,这2个指针被称作该对象的别名(alias),如果对其中一个指针进行写入,就会影响从另一个指针的读出。在一个函数中,编译器通常不知道哪一个指针是别名,哪一个不是;或哪一个指针有别名,哪一个没有,对任何一个指针的写入,都会影响从任何其他指针的读出,但这样会明显降低代码执行的效率。
void timer_v1 (int * timer1, int *timer2,int *step)
{
*timer1 += *step;
*timer2 += *step;
}
注意编译器转载step两次。通常,一种被称为公共子表达式消除(common subexpresion elimination)的编译器选项,可以使编译器优化*step,只要被装载一次,第二次使用时,其值将会被重复使用。但是,在这里编译器不能使用这种优化。指针timer1和指针step可能互为别名。话句话说,编译器不能确定对指针timer1的写入是否会影响从指针step的读出。在这种情况下,第二次*step的值将与第一次不同,将会是*timer1的值。这就使编译器不得不增加一条额外的load指令。
在 timer_v1中,使用一个局部变量temp来保存*step的值,编译器就不会担心别名的问题了。
void timer_v1 (int * timer1, int *timer2,int *step)
{
int temp = *step;
*timer1 += temp;
*timer2 += temp;
}
反汇编
总结
标签:嵌入式
原文地址:http://blog.csdn.net/sunheshan/article/details/44779947