盒子
导航
文章目录
  1. 内存编址
  2. 对齐系数
  3. 类型大小
  4. 成员对齐
  5. 结构对齐
  6. 代码分析(Struct Test1)
  7. 参考资料

理解C/C++结构体内存对齐

内存对齐是编译器做的事情,但程序员如果明白其中的原理,将有助于写出更好的程序。在C/C++的结构体(Struct)当中,各个不同类型的成员的先后排列次序极大的可能会影响到该结构体占用内存空间的大小。先来看一个例子,在这个例子当中定义两个结构体,里面的成员是一样的,区别只在于排列顺序不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

/* #pragma pack(8) */

/* Under x86-64bit CPU, Fedora22, GCC, default pack(8) */
int main(int argc, char *argv[])
{
struct Test1{ /* logical address start with 0 */
int ivar1; /* map address 0, size 4 Bytes */
char cvar1; /* map address 4, size 1 Bytes */
int ivar2; /* map address 8, size 4 Bytes */
double dvar1; /* map address 16, size 8 Bytes */
int *pivar; /* map address 24, size 8 Bytes(8x8bit=64bit) */
float *pfvar; /* map address 32, size 8 Bytes(8x8bit=64bit) */
char cvar2; /* map address 40, size 1 Bytes */
}; /* End of address 41, size: (41/8)*8+41%8=48 Bytes */

struct Test2 { /* logical address start with 0 */
char cvar1; /* map address 0, size 1 Byte */
char cvar2; /* map address 1, size 1 Byte */
int ivar1; /* map address 4, size 4 Bytes */
int ivar2; /* map address 8, size 4 Bytes */
double dvar1; /* map address 16, size 8 Bytes */
int *pivar; /* map address 24, size 8 Bytes */
float *pfvar; /* map address 32, size 8 Bytes */
}; /* End of address 40, size: (40/8)*8+40%8=40 Bytes */

printf("Size of struct Test1: %d Bytes.\n", sizeof(struct Test1));
printf("Size of struct Test2: %d Bytes.\n", sizeof(struct Test2));

return 0;
} /* main */

在Fedora22/Gcc下的测试:

1
2
3
4
5
[fury@localhost clabs]$ gcc -o exe struct.c -g
[fury@localhost clabs]$ ./exe
Size of struct Test1: 48 Bytes.
Size of struct Test2: 40 Bytes.
[fury@localhost clabs]$

可以看到,排列顺序的不同使得内存占用大小相差了8个字节。或者将结构体这么排列,虽然结果还是40Bytes,但成员的对齐已经发生变化了,经过对比或许你能理解得更清晰:

1
2
3
4
5
6
7
8
9
struct Test3 { /* logical address start with 0 */
int ivar1; /* map address 0, size 4 Bytes */
int ivar2; /* map address 4, size 4 Bytes */
double dvar1; /* map address 8, size 8 Bytes */
int *pivar; /* map address 16, size 8 Bytes */
float *pfvar; /* map address 24, size 8 Bytes */
char cvar1; /* map address 32, size 1 Byte */
char cvar2; /* map address 33, size 1 Byte */
}; /* End of address 34, size: (34/8)*8+34%8=40 Bytes */

到了这里,依靠代码的注释或许你已经看出一些端倪了。先解释几个关键点,然后再结合上面的代码例子来分析。

内存编址

x86架构CPU为例。内存基本单元的大小是1Byte(8bit),若是32位机即数据总线宽度为32位,可同时传输32个0/1位,以一个字节编一个地址,根据排列组合原理就有2^32个地址,每个地址是一个内存单元,大小为1Byte(8bit),则该CPU最大内存寻址能力将为2^32 x 8bit = 4 x 2^30 x 1Byte = 4 x 1G x 1Byte = 4GBytes(即4GB)。这就是通常所说的32机理论可使用最大内存为4GB,同理64位机就是2^64字节。
内存结构

对齐系数

每个编译器都有自己默认的对齐系数。以x86-64/Fedora22为例,gcc的默认对齐系数是8,为什么是8呢?因为机器是64位,而8位是一个字节,64bit/8bit=8Bytes,也就是说它一下子可以取8个字节。所以若是32位机,那么最佳的对齐系数应该为4。可以在代码中使用#pragma pack(n) /* n取1,2,4,8,16即2的整次幂 */来更改编译器默认的对齐系数。

类型大小

  1. char:1 Byte.
  2. short:2 Bytes.
  3. int:4 Bytes.
  4. long:4 Bytes. —— 32位机.
  5. long:8 Bytes. —— 64位机.
  6. float:4 Bytes.
  7. double:8 Bytes.
  8. long long:8 Bytes.
  9. long double:8 Bytes(VC++),12 Bytes(GCC). —— 32位机.
  10. long double:8 Bytes(VC++),16 Bytes(GCC). —— 64位机.
  11. pointer:4 Bytes. —— 32位机.
  12. pointer:8 Bytes. —— 64位机.

成员对齐

指结构体中数据成员的地址对齐。假定结构体的起始逻辑地址为0,各成员的偏移地址按顺序对齐,第一个成员对齐到0,接下来的成员对齐到Min(成员类型大小,对齐系数)的倍数地址。如在对齐系数为8的情况下,char可以对齐到任意一个地址,int应该对齐到4n(n取自然数),32位机指针对齐到4n,64位机指针对齐到8n,float对齐到4n,double对齐到8n,等等。

结构对齐

指结构体整体对齐。结构体的结束地址要对齐到Min(最大成员类型大小,对齐系数)的倍数地址。如对齐系数为8,假定结构体内只有char、int两种类型,则最大的类型为int即占据内存大小最大的类型为4字节,而Min(4,8)=4,所以结构体结束地址应该为4n(n取整数,n>1)。

下面以Test1为例来分析代码(对齐系数为8),Test2/Test3是一样的道理。

代码分析(Struct Test1)

  • int ivar1: 第一个成员,它将对齐到min(4,8)*n,对齐到地址0
  • char cvar1: 由于上一个成员占据了4个字节,所以它对齐到min(1,8)*n,对齐到地址4
  • int ivar2: 上一个成员占据1个字节,而5将是其最小开始地址,但要对齐到min(4,8)*n,所以这里将会对齐到地址8,于是你便可以知道在char cvar1之后,空出了3个字节空间;
  • double dvar1: 由上一个成员的分析,同理可知它将对齐到min(8,8)*n,对齐到地址16,前面空出4个字节;
  • int *pivar: 64位机器内指针为8字节,上一个成员占据8字节,它对齐到min(8,8)*n,对齐到地址24
  • float *pfvar: 也是指针,对齐到min(8,8)*n,对齐到地址32
  • char cvar2: 大小为1字节,上一个大小为8字节,这里对齐到min(1,8)*n,对齐到地址40
  • struct Test1: 最后一个成员大小为1字节,结构体结束于41。即到目前为止结构体已经占据从0到40共41个字节,开始做整体对齐,它应该对齐到min(max(member type size), 8)*nmin(8,8)*n,对齐到48,因为41往后8的最小倍数就是48;于是结构体占据的空间为0到47共48个字节,最后空出7个字节。

参照代码注释,即可同理分析struct Test2和struct Test3。注意:有一种理解误区是,单纯地将各个成员大小相加然后做整体对齐,这是错误的。关键点在于要理解是每个成员以及结构体本身的地址要映射到内存地址。通过更改对齐系数,会改变结构体占据内存的大小,很显然,当对齐系数为1的时候,大小就直接是各个成员大小之和了,但这样不见得会使程序的效率提高。

参考资料

  1. Data Structure Alignment
  2. Memory Layout of C Programs
  3. Data alignment: Straighten up and fly right