开发高效率的程序涉及很多方面,包括编程风格、算法实现、针对目标的特殊优化等。这部分主要从ARM的体系结构特点出发,介绍几个程序开发中的注意点。
1 变量定义
变量定义虽然很简单,但是也有很多值得注意的地方。先看下面一个例子:
这里定义的4个变量形式都一样,只是次序不同,却导致了在最终映像中不同的数据布局,如图1所示。显然,第二种方式节约了更多的存储器空间。
图1 变量在数据区里的布局
由此可见,在变量声明的时候,需要考虑怎样最佳地控制存储器布局。当然,编译器在一定程度上能够优化这类问题,但是最好的方法还是在编程的时候,把所有相同类型的变量放在一起定义。
第二个问题是局部变量的类型定义。一般情况下,人们总是设法使用short或char来定义变量以节省存储器空间;但是,当一个函数的局部变量数目有限时,编译器会把局部变量分配给内部寄存器,每个变量占用一个寄存器。这样,使用short和char型变量不但起不到节省空间的作用,还会带来其它的副作用,如图2所示。假定a1是任意可能的寄存器存储函数的局部变量。同样完成加1的操作,32位的int型变量最快,只用一条加法指令。而8位和16位变量,完成加法操作后,还需要在32位的寄存器中进行符号扩展,其中带符号的变量,要用逻辑左移(LSL)接算术右移(ASR)两条指令才能完成符号扩展;无符号的变量,要使用一条逻辑与(AND)指令对符号位进行清零。所以,使用32位的int或unsigned int局部变量最有效率。
图2 不同类型局部变量的编译结果
变量定义中,还有一个与习惯思维相悖的地方,是冗余局部变量的使用。一般情况下,程序员总是竭力避免使用冗余变量,以精简程序。通常情况下这是正确的,但是也有例外,请看下面一个例子:
int f(void);
int g(void); // f()和 g()不访问全局变量errs
int errs; // 全局变量
void test1(void)
{
errs += f();
errs += g();
}
void test2(void)
{
int localerrs = errs; // 定义冗余的局部变量
localerrs += f();
localerrs += g();
errs = localerrs;
}
在第一种情况test1()里,每次访问全局变量errs时,都要先从相应的存储器load到寄存器里,经f()或g()函数调用后再store回原来的存储器里面。在这个例子里,一共要进行两次这样的load/store操作。而在第二种情况test2()里,局部变量localerrs被分配以寄存器,这样一来,整个函数就只需要一次load/store全局变量存储器了。节省存储器访问的次数对于系统性能的提高是非常有好处的。
2 参数传递
在ARM的工具链里,定义了统一的函数过程调用标准ATPCS(ARM-Thumb Procedure Call Standard)。ATPCS定义了寄存器组中的{R0~R3}作为参数传递和结果返回寄存器,如果参数数目超过四个,则使用堆栈进行传递。我们知道内部寄存器的访问速度是远远大于存储器的,所以要尽量使参数传递在寄存器里面进行,即应尽量把函数的参数控制在四个以下。这是理解ATPCS后,应该实现的一种编程风格。但是,利用ATPCS我们还可以得到更多,我们可以用它来实现C与汇编之间直接的函数调用。如下面的例子:
从C中直接调用汇编函数
extern void strcopy(char *d, const char *s);
int main(void){
const char *src =“Source”;
char dest[10];
…
strcopy(dest, src);
…
}
AREA StrCopy, CODE, READONLY
EXPORT strcopy
strcopy
LDRB R2, [R1], #1
STRB R2, [R0], #1
CMP R2, #0
BNE strcopy
MOV PC, LR
END
这个例子中的函数strcopy(dest, src)用汇编来实现,根据ATPCS的定义,函数参数从左到右由寄存器进行传递,所以在汇编中可以直接由R0和R1进行引用。有了这条途径,在C和汇编之间进行相互调用就容易实现了。
3 循环条件
记数循环是程序中十分常用的流程控制结构。在C中,类似下面的for循环比比皆是:
for (loop = 1; loop <= limit; loop++)
这种累加计数的方法符合一般的自然思维习惯,所以比下面的递减计数方法使用得多:
for (loop = limit; loop != 0; loop--)
这两者在逻辑上并没有效率差异,但是映射到具体的体系结构中,就产生了很大的不同,如图3所示。
图3 不同的循环条件设置比较
从图3中可以发现,累加法比递减法多用了一条指令,当循环次数比较大的时候,这两段代码就会在性能上产生出明显的差异。分析其中的本质原因,当进行一个非零常数比较时,必须用专门的CMP指令来执行;而当一个变量与零进行比较时,ARM指令可以直接利用条件执行的特性(NE)来进行判别。
因此,在ARM的体系结构下编程,最好采用递减至零的方法来设置循环条件。
4 条件执行
上面已经提及了ARM指令的条件执行,充分利用这个特性,可以有助于缩短代码长度,优化流程控制。请看图4所示的例子。
图4中的流程是求R0和R1最大公约数的例子。流程图中有两次条件比较,怎样才能得到最优的结果呢?下面给出了解决方案:
Start CMP R0, R1
SUBLT R1, R1, R0
SUBGT R0, R0, R1
BNE Start
Stop
从这个例子得到启发,在充分利用条件执行情况下,可以从一次比较判断得到多个跳转分支。
不过成组的条件执行指令跟在一个比较指令后面,如果条件执行的语句太多,在性能上会有牺牲。一般一条跳转指令B最多耗费3个指令周期。如果一个条件执行指令组的数目超过3条,可以考虑用跳转指令来进行条件分支,有助于条件判决的速度。当然,这么做就增加了代码长度,属于代码长度跟执行性能之间的一个矛盾平衡问题。如果性能是首要问题,那么在C中的条件描述语句中,如if(条件描述),应尽可能简化条件描述,因为复杂的条件描述容易产生较长的条件执行指令组。
5 混合编程
汇编和C/C++语言的混合编程,在一个追求效率的程序中是比较常见的。前面已经讲到过,在汇编和C/C++之间进行函数调用时,要遵循ATPCS的定义。这里介绍在C/C++里加入汇编程序的两种方法:内联汇编(inline assemble)和嵌入式汇编(embedded assemble)。
内联汇编是指在C/C++函数定义中插入汇编语句的方法,如下面的例子:
void enable_IRQ(void){
int tmp;
__asm { // 内联汇编定义
MRS tmp, CPSR // 可以引用外部的C变量定义
BIC tmp, tmp, #0x80
MSR CPSR_c, tmp
}
}
内联汇编的用法跟真实汇编之间有很大区别,并且不支持Thumb。在内联汇编之中,不能直接访问物理寄存器(CPSR除外),即使使用寄存器名进行编程,也会被编译器进行重新分配。
与内联汇编不同,嵌入式汇编具有真实汇编的所有特性,同时支持ARM和Thumb,但是不能直接引用C/C++的变量定义,数据交换必须通过ATPCS进行。嵌入式汇编在形式上表现为独立定义的函数体,如下所示:
__asm int add(int i, int j) {// 定义嵌入式汇编
ADD R0, R0, R1 // i的值放入R0,j放R1,结果放R0
MOV PC, LR
}
void main() {
printf(“2345 + 67890 = %d\n”, add(12345, 67890));
}
灵活使用内联汇编和嵌入式汇编,可以帮助提高程序效率。
6 性能分析
很多时候需要对程序的执行效率和性能进行分析,直接测试当然是最真实的途径,但是这种方法除了在运行时间上进行定量外,很难得到确切的数据信息。而指令集仿真方法(ARMulator或 ISS),恰恰为程序执行过程中处理器的行为提供了一个参数统计方法。
图5是在ADS的ARMulator环境下,对某一段程序运行的统计情况。统计可以在执行代码流中任意选择一个参考点开始。
图5 ARMulator环境下的处理器运行状态分析
图5中所示是一段例程在ARM7TDMI上面运行的状态统计,可以计算出平均每条指令花费的处理器时钟为1.9左右,进行代码优化时,目标就是减少非顺序访问周期和内部等待周期数。
7 小结
代码优化是个很大的题目,这里只是抛砖引玉,索引几个要点进行讨论。至此,关于在ARM体系结构下进行嵌入式系统编程的6个专题已经全部结束。对此感兴趣的读者可以访问http://www.arm.com/arm/documentation?OpenDocument进一步了解更多有关ARM的技术资料。
