背景
C语言的位域用于描述结构体的指定字段占多少bit,使得多个字段可以存到一个字节里,也可以让一个字段占多个字节。它能减小结构体的内存占用,同时还能精确限定结构体字段的取值范围。
问题的提出
项目上有个硬件模块,它输入的是mesh
结构体,输出的是小图
结构体,软件根据小图
计算出下一轮的mesh
供硬件模块下一轮读入。
最近该模块为了节省内存,它将输出的数据弄成紧排(packed,每个字段不保证8bit对齐)的格式,这样要想访问就必须用到位域操作了
因为数据量比较大,因此软件访问位域的开销不可忽略,需要定量分析下。
分析
想到用空的calc
函数来对比分析,设计两个版本的calc
函数,一个是pack
版,一个是unpack
版,函数的操作仅仅是将小图
的两个字段赋值给mesh
的两个字段(中间有什么截断
或移位
全凭编译器决定),然后将两个函数反汇编
,看各自对应多少条汇编指令
,最后相减,就是访问位域的开销了。
测试代码
注意看,两个calc的C语言执行语句完全相同,差异只能是在汇编层面。
typedef struct _SmallPic_Pack {
unsigned int gray:20;
unsigned int dc:10;
unsigned int reserved:2;
} SmallPic_Pack;
typedef struct _SmallPic_Unpack {
unsigned int gray;
unsigned short dc;
unsigned short reserved;
} SmallPic_Unpack;
typedef struct _Mesh_Pack {
unsigned int gray:16;
unsigned int reserved1:4;
unsigned int dc:10;
unsigned int reserved2:2;
} Mesh_Pack;
typedef struct _Mesh_Unpack {
unsigned short gray;
unsigned short dc;
} Mesh_Unpack;
void calc_pack(SmallPic_Pack *pic, Mesh_Pack *mesh)
{
mesh->gray = pic->gray;
mesh->dc = pic->dc;
}
void calc_unpack(SmallPic_Unpack *pic, Mesh_Unpack *mesh)
{
mesh->gray = pic->gray;
mesh->dc = pic->dc;
}
int main() {
SmallPic_Pack pic_pack = {0, 0, 0};
SmallPic_Unpack pic_unpack = {0, 0, 0};
Mesh_Pack mesh_pack = {0, 0, 0, 0};
Mesh_Unpack mesh_unpack = {0, 0};
calc_pack(&pic_pack, &mesh_pack);
calc_unpack(&pic_unpack, &mesh_unpack);
return 0;
}
测试结果
x86-64反汇编对比
000000000000066a <calc_pack>:
66a: 55 push %rbp
66b: 48 89 e5 mov %rsp,%rbp
66e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
672: 48 89 75 f0 mov %rsi,-0x10(%rbp)
676: 48 8b 45 f8 mov -0x8(%rbp),%rax
67a: 8b 00 mov (%rax),%eax
67c: 25 ff ff 0f 00 and $0xfffff,%eax
681: 89 c2 mov %eax,%edx
683: 48 8b 45 f0 mov -0x10(%rbp),%rax
687: 66 89 10 mov %dx,(%rax)
68a: 48 8b 45 f8 mov -0x8(%rbp),%rax
68e: 0f b7 40 02 movzwl 0x2(%rax),%eax
692: 66 c1 e8 04 shr $0x4,%ax
696: 66 25 ff 03 and $0x3ff,%ax
69a: 48 8b 55 f0 mov -0x10(%rbp),%rdx
69e: 66 25 ff 03 and $0x3ff,%ax
6a2: c1 e0 04 shl $0x4,%eax
6a5: 89 c1 mov %eax,%ecx
6a7: 0f b7 42 02 movzwl 0x2(%rdx),%eax
6ab: 66 25 0f c0 and $0xc00f,%ax
6af: 09 c8 or %ecx,%eax
6b1: 66 89 42 02 mov %ax,0x2(%rdx)
6b5: 90 nop
6b6: 5d pop %rbp
6b7: c3 retq
00000000000006b8 <calc_unpack>:
6b8: 55 push %rbp
6b9: 48 89 e5 mov %rsp,%rbp
6bc: 48 89 7d f8 mov %rdi,-0x8(%rbp)
6c0: 48 89 75 f0 mov %rsi,-0x10(%rbp)
6c4: 48 8b 45 f8 mov -0x8(%rbp),%rax
6c8: 8b 00 mov (%rax),%eax
6ca: 89 c2 mov %eax,%edx
6cc: 48 8b 45 f0 mov -0x10(%rbp),%rax
6d0: 66 89 10 mov %dx,(%rax)
6d3: 48 8b 45 f8 mov -0x8(%rbp),%rax
6d7: 0f b7 50 04 movzwl 0x4(%rax),%edx
6db: 48 8b 45 f0 mov -0x10(%rbp),%rax
6df: 66 89 50 02 mov %dx,0x2(%rax)
6e3: 90 nop
6e4: 5d pop %rbp
6e5: c3 retq
可以看到,pack版比unpack多了9条指令
arm64反汇编对比
000000000040055c <calc_pack>:
40055c: d10043ff sub sp, sp, #0x10
400560: f90007e0 str x0, [sp, #8]
400564: f90003e1 str x1, [sp]
400568: f94007e0 ldr x0, [sp, #8]
40056c: b9400000 ldr w0, [x0]
400570: d3404c00 ubfx x0, x0, #0, #20
400574: 12003c01 and w1, w0, #0xffff
400578: f94003e0 ldr x0, [sp]
40057c: 79000001 strh w1, [x0]
400580: f94007e0 ldr x0, [sp, #8]
400584: 79400400 ldrh w0, [x0, #2]
400588: d3443400 ubfx x0, x0, #4, #10
40058c: 12003c02 and w2, w0, #0xffff
400590: f94003e1 ldr x1, [sp]
400594: 79400420 ldrh w0, [x1, #2]
400598: 331c2440 bfi w0, w2, #4, #10
40059c: 79000420 strh w0, [x1, #2]
4005a0: d503201f nop
4005a4: 910043ff add sp, sp, #0x10
4005a8: d65f03c0 ret
00000000004005ac <calc_unpack>:
4005ac: d10043ff sub sp, sp, #0x10
4005b0: f90007e0 str x0, [sp, #8]
4005b4: f90003e1 str x1, [sp]
4005b8: f94007e0 ldr x0, [sp, #8]
4005bc: b9400000 ldr w0, [x0]
4005c0: 12003c01 and w1, w0, #0xffff
4005c4: f94003e0 ldr x0, [sp]
4005c8: 79000001 strh w1, [x0]
4005cc: f94007e0 ldr x0, [sp, #8]
4005d0: 79400801 ldrh w1, [x0, #4]
4005d4: f94003e0 ldr x0, [sp]
4005d8: 79000401 strh w1, [x0, #2]
4005dc: d503201f nop
4005e0: 910043ff add sp, sp, #0x10
4005e4: d65f03c0 ret
可以看到,pack版只比unpack多了5条指令,看来arm这种寄存器多的CPU更适合多媒体类应用啊
测试总结
- 不论哪种CPU,pack版都比unpack版更耗CPU cycle,因为数据要8bit对齐才能计算或赋值,所以移位操作是无法避免的,硬件不做就得CPU做
- 横向比对x86和arm,发现unpack版二者所用指令差不多,但pack版明显arm指令更少。
总结
访问位域有开销,这包括移位、截断等操作,但考虑到内存局部性原理,这些操作都不影响cache,因此性能并不会差多少。
后记
位域在定义时,先定义的字段在低bit,然后逐渐往高bit放,别弄反了。这有个好处,就是不论是8位CPU还是64位CPU,字段的位置都跟定义顺序保持一致。