什么是字节对齐?
字节对齐(也叫内存对齐)是为了提高内存存取效率,便于CPU快速访问,在编译阶段优化内存存取的一项技术。
说白了,就是各种类型的数据按照“一定的规则”在内存空间上排列,而不是顺序的一个接一个排列,如果当前的内存地址不满足规则就跳过,空着不用,直到满足规则的内存地址出现才存储。
为什么要进行内存对齐?
平台原因(方便移植):不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能在某些地址处取特定类型的数据,否则抛出硬件异常,例如SPARC。
性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,访问未对齐的内存时,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
计算机CPU一次可以处理多个字节,就拿32位系统来说,CPUT一次可以处理32bit的数据,也就是4个字节。假设平台每次都从偶地址开始读取数据,有一个int型数据,存放在内存地址0x1的位置。CPU要读取这个int数据,需要先从地址0x0开始读取4字节数据,此时这个int型还有一个字节没有读到,就得再从地址0x4开始读取4字节数据,并且还要进行位操作,把两次读取的数据合并为一个int型数据。多次读取,还要移位!—— 太麻烦,效率又低。那怎么办呢?为了简单高效地读取数据,干脆在存储时把这个int数据放在地址0x4的位置,0x1、0x2、0x3的位置都空着,CPU直接从0x4取数据,只需一次就取到了这个数据,还不用进行位操作。简而言之,就是拿空间换时间。
字节对齐规则
1.内置基础数据类型对齐模数(有符号无符号相同)
存对齐对基础数据类型在内存中存放的位置进行了限制,要求这些数据的首地址必须是某个数N的倍数,N称为该数据类型的对齐模数(alignment modulus)。
2.指定对齐模数
我们给编译器指定的对齐模数(在VC中使用指令:#pragma pack(n),如果不指定,在VS2010默认为8)
3.有效对齐模数
指定对齐模数与类型自身对齐模数的较小的值,就是实际生效的对齐模数
4.自定义类型的对齐模数(struct、class)
自定义类型(如结构)的对齐模数等同于其成员中最大的有效对齐模数,且具有一下特点:
各个成员按照被声明的顺序在内存中顺序(升序)存储,第一个成员的地址和整个类型的地址相同;
每个成员按照自身的有效对齐模数分别对齐;
结构体长度必须是结构体对齐模数的整数倍,末尾不够的地方补齐空字节。
例如,
struct test_A {
char ch1; // 自身对齐模数1,指定对齐模数8,有效对齐模数1
char ch2; // 自身对齐模数1,指定对齐模数8,有效对齐模数1
int i1; // 自身对齐模数4,指定对齐模数8,有效对齐模数4
short sh1; // 自身对齐模数2,指定对齐模数8,有效对齐模数2
} st_test_A; // 自身对齐模数4,指定对齐模数8,有效对齐模数4
struct test_B {
char ch1;
char ch2;
short sh1;
int i1;
} st_test_B;
struct test_C {
char ch1;
char ch2;
char ch3;
} st_test_C[2];
printf("sizeof(test_A) = %d, addr st_test_A = %p\n", sizeof(test_A), &st_test_A);
printf(" sizeof(char) = %d, addr ch1 = %p\n", sizeof(char), &st_test_A.ch1);
printf(" sizeof(char) = %d, addr ch2 = %p\n", sizeof(char), &st_test_A.ch2);
printf(" sizeof(int) = %d, addr i1 = %p\n", sizeof(int), &st_test_A.i1);
printf(" sizeof(short) = %d, addr sh1 = %p\n\n", sizeof(short), &st_test_A.sh1);
printf("sizeof(test_B) = %d, addr st_test_B = %p\n", sizeof(test_B), &st_test_B);
printf(" sizeof(char) = %d, addr ch1 = %p\n", sizeof(char), &st_test_B.ch1);
printf(" sizeof(char) = %d, addr ch2 = %p\n", sizeof(char), &st_test_B.ch2);
printf(" sizeof(short) = %d, addr sh1 = %p\n", sizeof(short), &st_test_B.sh1);
printf(" sizeof(int) = %d, addr i1 = %p\n\n", sizeof(int), &st_test_B.i1);
printf("sizeof(test_C) = %d, addr st_test_C[0] = %p\n", sizeof(test_C), &st_test_C[0]);
printf(" sizeof(char) = %d, addr ch1 = %p\n", sizeof(char), &st_test_C[0].ch1);
printf(" sizeof(char) = %d, addr ch2 = %p\n", sizeof(char), &st_test_C[0].ch2);
printf(" sizeof(char) = %d, addr ch3 = %p\n", sizeof(char), &st_test_C[0].ch3);
printf("sizeof(test_C) = %d, addr st_test_C[1] = %p\n", sizeof(test_C), &st_test_C[1]);
printf(" sizeof(char) = %d, addr ch1 = %p\n", sizeof(char), &st_test_C[1].ch1);
printf(" sizeof(char) = %d, addr ch2 = %p\n", sizeof(char), &st_test_C[1].ch2);
printf(" sizeof(char) = %d, addr ch3 = %p\n\n", sizeof(char), &st_test_C[1].ch3);
在上面的例子中,如下图所示,st_test_A的起始地址为0x008FF9D0,是能被ch1的有效对齐模数1整除的地址,所以ch1存储在该地址,ch2同理;0x008FF9D2和0x008FF9D3都不能被i1的有效对齐模数4整除,因此不能存储i4,需要跳过,直到0x008FF9D4能被4整除,在该地址上存储i1。以此类推,在0x008FF9D8存储完最后一个成员sh1后,结构体的长度为10,不满足结构体对齐模数4的整数倍,因此需要在末尾在补上2个空字节。最终,st_test_A的长度为12。
从上面的例子我们不难发现,虽然st_test_A和st_test_B包含了一样的成员变量,但是两者的长度sizeof()却是不一样的。可见成员变量合理的声明顺序可以避免空字节的产生,节约存储空间。
VC中的字节对齐设置
在VS IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,Default是8字节。
在编码时,可以使用指令动态修改:#pragma pack
#pragma pack(4) // 指定按照4字节对齐,缺省为8字节对齐
// ...
#pragma pack() // 回复默认字节对齐,即8字节对齐