学习Cortex-M:结构体 vs 分散变量
静态变量使用结构体表示的代码无论在space上还是speed上都要优于分散变量,应当尽量使用结构体。对于局部变量采用结构体还是分散变量并没有什么区别。
mingdu.zheng at gmail dot com
结构体
创建一段测试代码,定义一个结构体test_t,包含4个成员变量,函数func给结构体的4个成员变量赋值。
// struct1.c
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;
}
编译,然后反汇编。
arm-none-eabi-gcc -o struct1.o struct1.c -c -O2 -Wall -mthumb -march=armv7-m
arm-none-eabi-objdump -d struct1.o
观察编译器生成的代码,除去函数调用的开销,函数体的实现用了5条指令,从第5行到第9行。第1条ldr指令加载结构体的地址,接下来的4条str指令进行结构体成员的赋值。str指令的地址偏移常量也就是各个成员变量在结构体中的偏移。
struct1.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <func>:
0: b410 push {r4}
2: 4c04 ldr r4, [pc, #16] ;加载结构体地址
4: 6060 str r0, [r4, #4] ;赋值a
6: 6021 str r1, [r4, #0] ;赋值b
8: 60a2 str r2, [r4, #8] ;赋值c
a: 60e3 str r3, [r4, #12] ;赋值d
c: f85d 4b04 ldr.w r4, [sp], #4
10: 4770 bx lr
12: bf00 nop
14: 00000000 .word 0x00000000
如果仔细观察,可以发现上面那段代码的赋值次序与成员变量在结构体中的排放次序是不一样的,现在来调整一下结构体成员的次序,让赋值次序与成员变量的排放次序相同。
// struct2.c
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;
}
重新编译和反汇编。
arm-none-eabi-gcc -o struct2.o struct2.c -c -O2 -Wall -mthumb -march=armv7-m
arm-none-eabi-objdump -d struct2.o
再来观察编译器的输出,这次生成的指令数更少了,只有2条指令,第5行和第6行。一条ldr指令加载结构体地址,这与上面的输出是一致的,另外一条是stmia指令,stmia指令厉害了,一条指令就把4个赋值全给搞定了。这就是赋值次序和成员变量次序一致的结果,又快又省。当然,现实中赋值次序和排列次序一致的情况还真不多见。所以还是struct1例子比较现实。
struct2.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <func>:
0: b410 push {r4}
2: 4c03 ldr r4, [pc, #12] ; (10 <func+0x10>)
4: e884 000f stmia.w r4, {r0, r1, r2, r3}
8: f85d 4b04 ldr.w r4, [sp], #4
c: 4770 bx lr
e: bf00 nop
10: 00000000 .word 0x00000000
分散变量
现在把结构体内的变量打散。
// discrete.c
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;
}
用同样的编译选项进行编译。
arm-none-eabi-gcc -o discrete.o -c discrete.c -Wall -mthumb -march=armv7-m -O2
arm-none-eabi-objdump -d discrete.o
是8条指令!从第5行到第12行。前面4条ldr指令分别加载4个变量的地址,后面4条str指令执行赋值。比struct1例子多了3条ldr指令。为什么会多了3条ldr指令呢?这4个变量也是按次序摆放的,是不是应该像struct2例子一样用STMIA实现?这是因为变量的存储位置不是编译器说了算的,而是链接器说了算的,在编译的时候,编译器不知道变量会被安排在什么位置上,只能假设各个变量的位置是没有关系的,所以要分别取4个变量的地址,然后才能赋值。而结构体成员的排列次序和偏移则是编译器说了算的,所以给出结构体时,编译器可以确定结构体成员的相对位置,只要取结构体首地址再加上各个成员的偏移就可以确定所有成员的地址了。链接器安排结构体时,是将其作为一个整体处理的,不会打散它。
discrete.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <func>:
0: b4f0 push {r4, r5, r6, r7}
2: 4f05 ldr r7, [pc, #20] ; (18 <func+0x18>)
4: 4e05 ldr r6, [pc, #20] ; (1c <func+0x1c>)
6: 4d06 ldr r5, [pc, #24] ; (20 <func+0x20>)
8: 4c06 ldr r4, [pc, #24] ; (24 <func+0x24>)
a: 6038 str r0, [r7, #0]
c: 6031 str r1, [r6, #0]
e: 602a str r2, [r5, #0]
10: 6023 str r3, [r4, #0]
12: bcf0 pop {r4, r5, r6, r7}
14: 4770 bx lr
优先使用结构体
从上面的分析可以看出使用结构体的代码无论在space上还是speed上都要优于分散变量,那还有什么理由不用结构体呢。应该尽量地将相关的变量组合成结构体,而不是以分散变量的形式。使用结构体还可以采用面向对象方法做设计,当需要多个实例的时候非常简单,再定义一个结构体实例就可以了,如果使用分散变量,那就惨了。提到了面向对象,难道当年ARM设计指令集的时候就是为面向对象编程优化的吗?
局部变量
上面的例子适用于静态变量(包括全局变量、文件域静态变量和函数域静态变量),再来看看局部变量的情况。
注意local例子的变量声明加上了volatile关键字,这个例子太简单,如果不加volatile关键字,那就等价于空函数,什么指令都不会生成的。
// local.c
void func(int a, int b, int c, int d)
{
volatile int la;
volatile int lb;
volatile int lc;
volatile int ld;
la = a;
lb = b;
lc = c;
ld = d;
}
用同样的参数进行编译和反汇编。
arm-none-eabi-gcc -o local.o -c local.c -Wall -mthumb -march=armv7-m -O2
arm-none-eabi-objdump -d local.o
4条str指令搞定,从第5行到第8行,ldr指令都省了。这是因为局部变量被安排在堆栈的函数调用帧中,基地址就是调用帧首地址,也就是sp寄存器的值。局部变量在调用帧中的位置是编译器决定的,所有局部变量都会被连续地存放。从这里可以得出结论,结构体优先原则不适用于局部变量。
local.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <func>:
0: b084 sub sp, #16
2: 9000 str r0, [sp, #0]
4: 9101 str r1, [sp, #4]
6: 9202 str r2, [sp, #8]
8: 9303 str r3, [sp, #12]
a: b004 add sp, #16
c: 4770 bx lr
ARMv6-M
ARMv6-M,即Cortex-M0和Cortex-M0+同样也支持STM指令,因此生成的代码和ARMv7-M差不多,细节上会稍有差别,毕竟ARMv6-M的大部分指令是16位的,而ARMv7-M则有大量的32位指令。