内存对齐
内存对齐”应该是编译器的“管辖范围”。编译器为程序中的每个“数据单元”安排在适当的位置上。
对于内存对齐问题,主要存在于struct和union等复合结构在内存中的分布情况,许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们要求这些数据的首地址的值是某个数M(通常是4或8);对于内存对齐,主要是为了提高程序的性能,数据结构,特别是栈,应尽可能在自然边界上对齐,经过对齐后,cpu的内存访问速度大大提升。
Windows中默认对齐数为8,Linux中默认对齐数为4;
内存对齐的主要作用(原因)
1、平台原因(移植原因)
- 不是所有的硬件平台都能访问任意地址上的任意数据的;
- 某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因
- 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
- 为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
内存对齐规则
编译器中提供了#pragma pack(n)来设定变量以n字节对齐方式
在内存中,编译器按照成员列表顺序分别为每个结构体变量成员分配内存,当存储过程中需要满足边界对齐的要求时,编译器会在成员之间留下额外的内存空间。如果想确认结构体占多少存储空间,则使用关键字sizeof,如果想得知结构体的某个特定成员在结构体的位置,则使用offsetof宏(定义于stddef.h)。
- 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。即排列在后面的成员其当前偏移量必须是当前成员类型的整数倍。
- 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
- 结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
总结:
内存对齐的原则:
- 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
- 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
- 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。
假如我们的一个结构体有1个int,两个char, 那么不同的排列顺序,会造成结构体的大小不一致。
#include<iostream>
using namespace std;
struct A{
char a;
int b;
short c;
};
struct B{
short c;
char a;
int b;
};
int main(){
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
return 0;
}
以上面结构体A为例,第一个成员a是char类型,占用1个字节空间,偏移量为0,第二个成员b是int类型,占用4个字节空间,按照规则1,b的偏移量必须是int类型的整数倍,所以编译器会在a变量后面插入3字节缓冲区,保证此时b的偏移量(4字节)是b类型的整数倍(当前恰好是1倍),第3个成员c为short类型,此时c的偏移量正好是4+4=8个字节,已经是short类型的整数倍,故b与c之间不用填充缓冲字节。但这时,结构体A的大小为8+2=10个字节,按照规则2,结构体A大小必须是其最大成员类型int的整数倍,所以在10个字节的基础上再填充2个字节,保证最后结构体大小为12,以符合规则2.
数据成员- | -前面偏移量- | - 成员自身占用 |
---|---|---|
(char) a | 0 | 1 |
缓冲补齐 | 1 | 3(规则1) |
(int) b | 4 | 4 |
(short) c | 8 | 2 |
缓冲补齐 | 10 | 2(规则2) |
类似的,结构体B成员的分析如下:
数据成员- | -前面偏移量- | -成员自身占用 |
---|---|---|
short c | 0 | 2 |
char a | 2 | 1 |
缓冲补齐 | 3 | 1(规则1) |
int b | 4 | 4 |
另一个更复杂的例子:
struct BU
{
int number; //4字节
union UBffer
{
char buffer[13]; //填充3字节,该成员占16字节空间
int number;
}ubuf;
int aa; //占4字节空间,当前偏移量已补齐为20
double dou; //占8字节空间
}bu;//sizeof(BU) = 4 + 13 + 3(补齐) + 4 + 8 = 32
结构体BU稍微变换下aa和dou成员顺序,则结果就大不相同:
struct BC
{
int number; //4字节
union UBffer
{
char buffer[13]; //填充7字节,该成员占20字节空间
int number;
}ubuf;
double dou; //占8字节空间,当前偏移量已补齐为24
int aa; //占4字节空间,当前占用空间36字节,最大double类型,还需要根据规则2补齐
}bu;//sizeof(BC) = 4 + 13 + 7(规则1补齐) + 8 + 4 + 4(规则2补齐) = 40 (8的整数倍)
我们可能对于结构体类包含union类型成员抱有疑虑,再考虑下面实例:
struct BD
{
short number;
union UBffer
{
char buffer[13];
int number;
}ubuf;
}bc;
运行结果是sizeof(BD) = 2 + 2 + 13 +3 = 20,可能你会问,为什么不是2+13+1 = 16,这是因为union类型比较特殊,计算union成员的偏移量时,需要根据union内部最大成员类型来进行缓冲补齐,所以为了保证偏移量为union最大成员int类型的整数倍,需要在number(short类型)后面填充2个字节,前面例子中number是int类型,就没有这个必要了。
再比如:
struct BE
{
short number;
union UBffer
{
char buffer[13];
double number;
}ubuf;
}bc;
它的运行结果是sizeof(BE) = 2 + 6 + 13 + 3 = 24,number后面为了与double类型进行对齐而补齐了6个字节,最后再按照规则2补齐了3个字节。
考虑规则3:
举个例子,在#pragma pack(1)时,以1个字节对齐时,属于最简单的情况,结构体大小是所有成员的类型大小的和。所以sizeof(BU) = sizeof(BC) = 29,这时与成员变量顺序不再相关。其他指定的字节对齐也很好分析。一般而言,奇数个字节对齐没有意义,正常情况下,编码人员不关心编译器对内存对齐所作的工作。
内存对齐的优点
- 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
- 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。