分析C语言位域的访问开销

背景

C语言的位域用于描述结构体的指定字段占多少bit,使得多个字段可以存到一个字节里,也可以让一个字段占多个字节。它能减小结构体的内存占用,同时还能精确限定结构体字段的取值范围。

问题的提出

项目上有个硬件模块,它输入的是mesh结构体,输出的是小图结构体,软件根据小图计算出下一轮的mesh供硬件模块下一轮读入。
最近该模块为了节省内存,它将输出的数据弄成紧排(packed,每个字段不保证8bit对齐)的格式,这样要想访问就必须用到位域操作了
分析C语言位域的访问开销_第1张图片
因为数据量比较大,因此软件访问位域的开销不可忽略,需要定量分析下。

分析

想到用空的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 :
 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 :
 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 :
  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 :
  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更适合多媒体类应用啊

测试总结

  1. 不论哪种CPU,pack版都比unpack版更耗CPU cycle,因为数据要8bit对齐才能计算或赋值,所以移位操作是无法避免的,硬件不做就得CPU做
  2. 横向比对x86和arm,发现unpack版二者所用指令差不多,但pack版明显arm指令更少。

总结

访问位域有开销,这包括移位、截断等操作,但考虑到内存局部性原理,这些操作都不影响cache,因此性能并不会差多少。

后记

位域在定义时,先定义的字段在低bit,然后逐渐往高bit放,别弄反了。这有个好处,就是不论是8位CPU还是64位CPU,字段的位置都跟定义顺序保持一致。

你可能感兴趣的:(C/C++,c语言,位域)