以下运行环境为Cortex-M4内核,编译环境为Keil MDK并且优化级别为-O2。
静态变量使用结构体表示的代码无论在space上还是speed上都要优于分散变量,应当尽量使用结构体。对于局部变量采用结构体还是分散变量并没有什么区别。
结构体
创建一段测试代码,定义一个结构体test_t,包含4个成员变量,函数func给结构体的4个成员变量赋值。
struct test_t
{
int b;
int a;
int c;
int d;
};
struct test_t test;
void func(int a, int b, int c, int d)
{
test.a = a;
test.b = b;
test.c = c;
test.d = d;
}
生成的汇编代码如下:
0: PUSH {r4,lr}
2: LDR r4, [pc, #16] ;加载结构体地址
4: STR r0, [r4, #0x04] ;赋值a
6: STR r1, [r4, #0x00] ;赋值b
8: STR r2, [r4, #0x08] ;赋值c
a: STR r3, [r4, #0x0c] ;赋值d
c: POP {r4,pc}
观察编译器生成的代码,除去函数调用的开销,函数体的实现用了5条指令,从第2行到第6行。第1条LDR指令加载结构体的地址,接下来的4条STR指令进行结构体成员的赋值。STR指令的地址偏移常量也就是各个成员变量在结构体中的偏移。
如果仔细观察,可以发现上面那段代码的赋值次序与成员变量在结构体中的排放次序是不一样的,现在来调整一下结构体成员的次序,让赋值次序与成员变量的排放次序相同。
struct test_t
{
int a;
int b;
int c;
int d;
};
struct test_t test;
void func(int a, int b, int c, int d)
{
test.a = a;
test.b = b;
test.c = c;
test.d = d;
}
生成的汇编代码如下:
0: PUSH {r4,lr}
2: LDR r4, [pc, #12] ;加载结构体地址
4: STM r4, [r4, r0-r3]
8: POP {r4,pc}
再来观察编译器的输出,这次生成的指令数更少了,只有2条指令,第2行和第3行。一条lLDR指令加载结构体地址,这与上面的输出是一致的,另外一条是STM指令,STM指令厉害了,一条指令就把4个赋值全给搞定了。这就是赋值次序和成员变量次序一致的结果,又快又省。当然,现实中赋值次序和排列次序一致的情况还真不多见。所以还是上面的例子比较现实。
分散变量
现在把结构体内的变量打散。
int ga;
int gb;
int gc;
int gd;
void func(int a, int b, int c, int d)
{
ga = a;
gb = b;
gc = c;
gd = d;
}
如果上面的全局变量在文件中的位置都都相邻,生成的汇编代码与上面是一样的。如果各个变量都不相邻,每条C语言语句就生成两条汇编指令,代码如下:
0: LDR r4, [pc, #12] ;加载结构体地址
2: STR r4, [r4, #00]
从上面的分析可以看出使用结构体或者对相邻静态变量赋值,无论在space上还是speed上都要优于分散变量。
局部变量
上面的例子适用于静态变量(包括全局变量、文件域静态变量和函数域静态变量),再来看看局部变量的情况。
注意local例子的变量声明加上了volatile关键字,这个例子太简单,如果不加volatile关键字,那就等价于空函数,什么指令都不会生成的。
0: PUSH {r0-r3,lr}
2: STM sp,{r0-r3}
6: POP {r0-r3,pc}
1条STM指令搞定(如果变量声明与赋值顺序不一样,就换转化为4条LDR指令),ldr指令都省了。这是因为局部变量被安排在堆栈的函数调用帧中,基地址就是调用帧首地址,也就是sp寄存器的值。局部变量在调用帧中的位置是编译器决定的,所有局部变量都会被连续地存放。从这里可以得出结论,结构体优先原则不适用于局部变量。
Cortex-M0
Cortex-M0和Cortex-M0+同样也支持STM指令,因此生成的代码与M4差不多,细节上会稍有差别,毕竟M0的大部分指令是16位的,而M4则有大量的32位指令。