1.什么是内存对齐
程序定义的数据都要存放到内存中,如果数据起始的内存地址是自身长度的整数倍那么该数据的内存是对齐的。
2.为什么要内存对齐
- 因为某些cpu不支持非内存对齐的数据
- 即使cpu支持非内存对齐的数据,读写的效率也会比内存对齐的数据慢很多,为了追求高效,我们需要做内存对齐。
3.为什么非对齐的数据读取的效率就低呢?
比如32位的cpu它的数据总线是32位,每次cpu都会读取4个字节的数据。cpu会根据地址总线确定读取的位置。
如果数据排布是左边这种情况(非内存对齐),那么cpu需要通过数据总线读取两次,才能获得完整的数据。
如果数据排布是右边这种情况(内存对齐),那么cpu只需要读取一次就可以获得完整的数据。
在研究这个问题的时候无意间发现一个叫做Nand to Tetris的课程。
Nand to Tetris课程将从布尔代数和初级逻辑门一路走到建立一个中央处理单元,一个存储系统和一个硬件平台,最终制作一个可以运行任何程序的通用计算机。
课程可以在Coursera上进行学习。
4.结构体内存对齐原则
结构体对齐分默认对齐和自定义对齐,我们先讨论默认对齐的情况。
默认对齐原则
1.结构体中每一个成员变量的内存都要对齐。
假设结构体第一个变量分配的偏移地址是零,那么后面变量分配的偏移地址一定是自身长度的整数倍,如果不是那么就要补齐地址后再分配这个变量。
struct s
{
char c;
int i;
};
char c是第一个变量不用考虑对齐,直接分配地址0x00。
int i 要分配一个满足对齐的地址0x04,所以要补齐3位,i被分配到地址0x04上。
sizeof(s) = 4 + 4 = 8
2.结构体的大小要满足最大成员的整数倍
struct s
{
int i;
char c;
};
i先分配四个字节
c从0x04开始占用1个字节
sizeof(s) = 4 + 1 = 5
5这个数字在内存中看着就不友好,根据第2条原则,结构体的大小是最大成员的整数倍,最大成员是四个字节的int型,所以在c的后面要补齐3个字节,最终sizeof(s) = 8。
3.结构体的地址是结构体中最大成员的整数倍
struct s1
{
int d;
};
struct s
{
int i;
s1 s1;
char c;
};
i 4个字节
s1 4个字节
c 1个字节
sizeof(s) = 4 + 4 + 1 = 9
根据原则2补齐到4的倍数所以大小是12
struct s1
{
double d1;
int i1;
};
struct s
{
int i;
s1 s1;
char c;
};
i 占4个字节
s1是一个结构体,根据原则三s1最大的成员是double所以s1的首地址要是double型的整数倍。所以补齐4个字节。
s1是 8 + 4 +4(根据原则2补齐)= 16
sizeof(s) = 4 + 4(补齐) + 16 + 1 = 25
这里有个问题结构体s的整数倍是按照int还是s1中的double?
结果sizeof(s) = 32 说明结构体中最大的成员可以是嵌套的结构体中最大的成员,很绕口。我们可以把s1当作是一个特殊类型,这个类型的对齐系数就是结构体中最大的成员的长度。上面代码取的就是s1中的d1做为最大的成员,所以最后补齐了7个字节。
自定义原则
之前都是默认对齐,有的时候我们希望自己能够控制对齐的方式,比如压缩空间,时间换空间。
#pragma pack (n) 自定义n字节对齐
#pragma pack () 取消自定义对齐
#pragma pack (push,n) 自定义n字节对齐,并将之前的对齐系数压入栈中
#pragma pack(pop) 恢复对齐系数
这两组命令没有什么区别,只是后则可以恢复之前的对齐系数。
对齐系数n会和变量自身做比较,选择较小的做为变量的对齐系数。
对齐系数n会和结构体最大的成员做比较,选择较小的做为结构体的对齐系数。
#pragma pack(1)
struct s
{
int i;
char c;
};
sizeof(s) = 5
i占4个字节
c紧接着i占1个字节
1(n) < 4(int)所以结构体的对齐系数是1
#pragma pack(2)
struct s
{
char i;
double d;
int c;
};
i分配1个字节
d自身的对齐系数是8,大于n,所以选择2做为对齐系数,补齐1个字节。
c自身的对齐系数是4,大于n,所以选择2做为对齐系数,当前的偏移量是10,满足对齐,所以c紧接着d分配四个字节。
sizeof(s) = 1 + (1) + 8 + 4 = 14
如果把n改成4呢?
sizeof(s) = 1 + (3) + 8 + 4 = 16
如果把n改成8呢?
sizeof(s) = 1 + (7) + 8 + 4 = 20
结构体的对齐系数是8,所以最终的sizeof(s) = 24
#pragma pack(4)
struct s1
{
double d1;
char str[13];
};
struct s
{
char i;
double d;
s1 s1;
int c;
};
sizeof(s) = 1 + (3) + 8 + (8 + 13 +(3)) + 4 = 40
有的时候我们想使用sse指令集进行向量运算,每一个向量需要按照16字节对齐(128位寄存器)。
与#pragma pack (n)用来压缩内存相反__declspec(align(n))可以扩大对齐系数
struct s1
{
double d1;
char str[13];
};
struct __declspec(align(16)) s
{
char i;
double d;
s1 s1;
int c;
};
sizeof(s) = 48 其他计算过程都没变,只是在最后计算结构体对齐的时候对齐系数是16。所以结构体的大小由40变成了48。
#pragma pack(4)
struct __declspec(align(16)) s1
{
double d1;
char str[13];
};
struct s
{
char i;
double d;
s1 s1;
int c;
};
i分配一个字节1
d补齐3个字节
s1紧跟d占用8 + 13 + 9 = 32个字节
c占4个字节
sizeof(c) = 1 + (3) + 8 + (8 + 13 + 9) + 4 = 48
实际结果是sizeof(c) = 64
哈哈哈晕了是不是。
i分配一个字节
d补齐3个字节
s1的对齐系数是16,这时候的#pragma pack(4)对s1补启作用了,所以s1要补齐3个字节到16。
c占4个字节
sizeof(c) = 1 + (3) + 8 + (4) + (8 + 13 + 9) + 4 = 52
因为s1使用了 __declspec(align(16))所以整个结构体都要按照16字节对齐 = 64。
这种情况正常编程应该不会这么写,但是如果嵌套多了不注意会写成这样。