内存中的数据对齐问题

对CPU而言为了取值效率,一般需要你的数据的内存地址必须是对齐(align)的,这样可以加快程序的运行速度。虽然对齐会消耗的额外内存空间,但对于程序速度的提升来说,是值得的。

所谓对齐,就是每份数据的地址必须能整除2的n次方,这个2n就是对齐参数(alignment value),单位为Byte。对齐参数由编译器默认设置,当然你也可以自己指定。具体的对齐规则如下:

  • 在结构体内部,第一个成员偏移地址为0,其它成员偏移地址必须是它 自身大小对齐参数 两者中较小那个值的整数倍;
  • 在计算结构体自身长度时,取 所用过的对齐值中最大值 的整数倍;
  • 在结构体之间,各结构体的偏移地址必须是 各结构体中最大长度成员对齐参数 两者中较小的那个值的整数倍。

对gcc来说,可以使用预编译指令 #pragma 或者gcc特有的关键字 __attribute__1 两种方式设置。

#pragma pack(n)

声明#pragma可以设置对齐参数的数值,缺省是8字节:

#pragma pack( [show] | [push | pop] [, identifier], n )

声明#pragma pack()表示通知编译器恢复成默认对齐参数。

一个结构体示例:

#pragma pack(4)
struct foo
{
    char a;
    short b;
    double c;
    char d;
};

设置对齐参数为4字节,则各成员占用内存空间如下:

  • a 第一个成员,偏移为0,占用1字节 [0]
  • b 自身大小为2字节,小于对齐参数,则对齐值取2,偏移为2,占用2字节 [2-3]
  • c 自身大小为8字节,大于对齐参数,则对齐值取4,偏移为4,占用8字节 [4-11]
  • d 自身大小为1字节,小于对齐参数,则对齐值取1,偏移为12,占用1字节 [12]

最终,计算该结构体长度,用到过的对齐值最大为4,则补齐整数倍到16,即该结构体长度。

然后,再看一个复合的结构体:

struct bar
{
    short a;
};
struct foobar
{
    struct foo f;
    struct bar b;
};

结构体bar只有一个成员,对齐值为2字节,该结构体的长度也是2字节。结构体foobar中各成员结构体里最大长度成员是double类型,长度为8字节,对齐参数为4字节,取小值。于是,可以得知结构体foobar的长度需要补齐到4的整数倍,即20字节。

attribute((packed))

在gcc里还可以使用__attribute__关键字来声明数据类型的对齐方式,优先级高于#pragma预编译指令。

不过需要注意的是 __attribute__((aligned(1)))__attribute__((packed)) 在使用的时候还是有些区别的。前者告诉编译器每个结构体成员的偏移地址都可以紧凑地从前一个成员结束处开始,但并不指定该成员本身可以占用多少空间,这意味着编译器在该结构体末尾可以自己补齐到4字节的整数倍。而后者告诉编译器禁止自主补齐,成员占了多少空间,即等于该结构体占用的空间。