最近在调试中遇到点内存对齐的问题,别人问我是怎么回事,我赶紧偷偷查了一下,记录下来。
不论是C、C++对于内存对齐的问题在原理上是一致的,对齐的原因和表现,简单总结一下,以便朋友们共享。
一、内存对齐的原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
也有的朋友说,内存对齐出于对读取的效率和数据的安全的考虑,我觉得也有一定的道理。
二、对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。比如32位windows平台下,VC默认是按照8bytes对齐的(VC->Project->settings->c/c++->Code Generation中的truct member alignment 值默认是8),程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
在嵌入式环境下,对齐往往与数据类型有关,特别是C编译器对缺省的结构成员自然对届条件为“N字节对 齐”,N即该成员数据类型的长度。如int型成员的自然对界条件为4字节对齐,而double类型的结构成员的自然对界条件为8字节对齐。若该成员的起始 偏移不位于该成员的“默认自然对界条件”上,则在前一个节面后面添加适当个数的空字节。C编译器缺省的结构整体的自然对界条件为:该结构所有成员中要求的 最大自然对界条件。若结构体各成员长度之和不为“结构整体自然对界条件的整数倍,则在最后一个成员后填充空字节。
那么可以得到如下的小结:
类型 对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char 偏移量必须为sizeof(char)即1的倍数
Short 偏移量必须为sizeof(short)即2的倍数
int 偏移量必须为sizeof(int)即4的倍数
float 偏移量必须为sizeof(float)即4的倍数
double 偏移量必须为sizeof(double)即8的倍数
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充。同时为了确保结构的大小为结 构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节,也就是 说:结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。对于char数组,字节宽度仍然认为为1。
对于下述的一个结构体,其对齐方式为:
struct Node1{
double m1;
char m2;
int m3;
};
对于第一个变量m1,sizeof(double)=8个字节;接下来为第二个成员m2分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把m2存放在偏移量为8的地方满足对齐方式,该成员变量占用 sizeof(char)=1个字节;接下来为第三个成员m3分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof (int)=4的倍数,为了满足对齐方式对偏移量的约束问题,自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int), 由于8+4+4 = 16恰好是结构体中最大空间类型double(8)的倍数,所以sizeof(Node1) =16.
typedef struct{
char a;
int b;
char c;
}Node2;
成员a占一个字节,所以a放在了第1位的位置;由于第二个变量b占4个字节,为保证起始位置是4(sizeof(b))的倍数,所以需要在a后面填充3个 字节,也就是b放在了从第5位到第8位的位置,然后就是c放在了9的位置,此时4+4+1=9。接下来考虑字节边界数,9并不是最大空间类型int(4) 的倍数,应该取大于9且是4的的最小整数12,所以sizeof(Node2) = 12.
typedef struct{
char a;
char b;
int c;
}Node3;
明显地:sizeof(Node3) = 8
对于结构体A中包含结构体B的情况,将结构体A中的结构体成员B中的最宽的数据类型作为该结构体成员B的数据宽度,同时结构体成员B必须满足上述对齐的规定。
要注意在VC中有一个对齐系数的概念,若设置了对齐系数,那么上述描述的对齐方式,则不适合。
例如:
1字节对齐(#pragma pack(1))
输出结果:sizeof(struct test_t) = 8 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(1)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=8;
2字节对齐(#pragma pack(2))
输出结果:sizeof(struct test_t) = 10 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(2)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;
4字节对齐(#pragma pack(4))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(4)
struct test_t { //按几对齐, 偏移量为后边第一个取模为零的。
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;
8字节对齐(#pragma pack(8))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(8)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;
16字节对齐(#pragma pack(16))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(16)
struct test_t {
int a;
char b;
short c;
char d;
};
#pragma pack()
成员总大小=9;
至于8字节对齐和16字节对齐,我觉得这两个例子取得不好,没有太大的参考意义。
(x666f)