结构体内存对齐和填充

结构体的大小

要了解结构内存对齐和填充方式,我们要先来看一个示例:
猜猜下面的结构体(32位计算机)所占内存空间大小?

#include <stdio.h>
 
struct test1
{
    char x;
    int y;
};
 
struct test2 
{ 
   char x; 
   short int y;
};
 
struct test3 
{ 
  char x;
  double y;  
  short int z; 
}; 
 
struct test4 
{ 
  double y;  
  short int z; 
  char x;
}; 
int main(void) 
{
	printf("sizeof(struct test1 )=%d\n",sizeof(struct test1 ));
	printf("sizeof(struct test2 )=%d\n",sizeof(struct test2 ));
	printf("sizeof(struct test3 )=%d\n",sizeof(struct test3 ));
	printf("sizeof(struct test4 )=%d\n",sizeof(struct test4 ));
	return 0;
}

预期输出结果:

sizeof(struct test1 )= sizeof(char)+sizeof(int)=1+4=5
sizeof(struct test2 )=sizeof(char)+sizeof(short int)=1+2=3
sizeof(struct test3 )=sizeof(char)+sizeof(double)+sizeof(short int)=1+8+2=11
sizeof(struct test4 )=sizeof(double)+sizeof(short int)+sizeof(char)=8+2+1=11

实际输出结果:

sizeof(struct test1 )=8
sizeof(struct test2 )=4
sizeof(struct test3 )=24
sizeof(struct test4 )=16

实际输出与预期值不同的原因是由于结构体的成员所占的内存空间需要对齐和填充。
对齐不是修饰或语言的要求,因为处理器架构的缘故,它是必需的功能,可以节省执行周期。

什么是内存对齐

通用处理器有一个单独的外围单元来存储数据,称为内存(Memory)。内存基本上是字节对齐的,对齐的字节长度取决于地址总线。地址总线用于将地址请求从CPU传输到内存,而数据总线用于从内存请求的地址读取数据或向内存请求的地址写入数据。

例:
对于16位处理器->地址总线为16位->字节长度= 16位。
对于32位处理器->地址总线为32位->字节长度= 32位。
对于64位处理器->地址总线为64位->字节长度= 64位。

那么,让我们看看地址总线宽度对我们有意义吗?

我们知道,指令执行所需机器周期数=数据字节长度/地址总线宽度(以位为单位),这意味着,地址总线越宽,所需指令执行机器周期越少,所花时间就越短。

例:

处理32位数据需要多少个机器周期?

16位机器,所需的机器周期数= 32/16 = 2个机器周期
32位机器,所需的机器周期数= 32/32 = 1个机器周期
64位机器,所需的机器周期数= 32/64 = 1个机器周期

数据对齐

数据对齐采用填充数据的方式使每个数据类型在结构上字对齐。经过对齐后的数据,处理器将变量的数据存储到内存中,从而可以最大程度地提高数据传输效率。

数据类型 Size(32-bit) 存储基地址(用于对齐)
char 1 任何地方
short int 2 基址必须被2整除
int 4 基址必须被4整除
long 4 基址必须被4整除
float 4 基址必须被4整除
double 8 基址必须被8整除

数据填充

由于结构体中的成员类型不一致,为提高计算机读取和写入效率。计算机在较短的数据类型尾部插入保留字使之实现内存中数据字对齐。

例1:

struct test1
{
    char x;
    char padded_byte[3]; // 填充3个字节使之与后面的整型变量内存对齐
    int y;
};

说明:

地址 成员 说明
0x00000000 x char数据可以存储在内存中的任何位置
0x00000001 padded_byte [0] int成员不能存储在这里,因为它不能被4整除
0x00000002 padded_byte [1] int成员不能存储在这里,因为它不能被4整除
0x00000003 padded_byte [2] int成员不能存储在这里,因为它不能被4整除
0x00000004 4 int成员可以存储在这里,因为它可以被4整除
总大小 8个字节 1(x) +3(填充) +4(y)

例2:

struct test2 
{ 
   char x; 
   char padded_Byte;   /*填充1个字节使之与short int 数据类型对齐*/ 
   short int y;
};

说明:

地址 成员 说明
0x00000000 x char数据可以存储在内存中的任何位置
0x00000001 padded_byte [0] short int成员不能存储在这里,因为它不能被2整除
0x00000002 padded_byte [0] short int成员可以存储在这里,因为它能被2整除
总大小 4个字节 1(x) +1(填充) +2(y)

例3:

struct test3 
{ 
  char x;
  char padded_byte_0[7];
  double y;  
  short int z; 
  char padded_byte_1[6];
};

说明:

地址 成员 说明
0x00000000 x char数据可以存储在内存中的任何位置
0x00000001-0x00000007 padded_byte_0[0]-padded_byte_0[7] double类型成员不能存储在这里,因为它不能被8整除
0x00000008-0x0000000F y double类型成员可以存储在这里,因为它能被8整除
0x00000010-0x00000011 z short int类型成员可以存储在这里,因为它能被2整除
0x00000012-0x0000001F padded_byte_1[0]-padded_byte_1[5] 填充6个字节使之保持对齐
总大小 24个字节 1(x)+7(papped_0)+8(y)+2(z)+6(papped_1)

数据打包

我们上面已经说过,数据对齐能够提升性能,但这会浪费大量内存。一些处理器会更多地关注内存大小而不是性能,然后限制对齐或使用#pragma之类的方法来确定自己的对齐边界以减少内存填充。这种将数据自定义打包到内存的过程称为数据打包。

#pragma的使用

语法:

pragma pack(alignment_byte)

示例:

#include <stdio.h>
 
#pragma pack (2)
 
struct test1
{
    char x;
    int y;
};
 
struct test2 
{ 
   char x; 
   short int y;
};
 
struct test3 
{ 
  char x;
  double y;  
  short int z; 
}; 
 
struct test4 
{ 
  double y;  
  short int z; 
  char x;
}; 
int main(void) 
{
    printf("sizeof(struct test1 )=%d\n",sizeof(struct test1 ));
    printf("sizeof(struct test2 )=%d\n",sizeof(struct test2 ));
    printf("sizeof(struct test3 )=%d\n",sizeof(struct test3 ));
    printf("sizeof(struct test4 )=%d\n",sizeof(struct test4 ));
    return 0;
}

输出结果:

sizeof(struct test1 )=6
sizeof(struct test2 )=4
sizeof(struct test3 )=12
sizeof(struct test4 )=12

从上面的例子,我们可以看到每个结构成员都以2字节边界对齐。你还可以测试其他对齐边界,例如1个字节,4个字节等。

下一节