序言
相信大家已经看过不少关于如何计算内存对齐的文章了,但是大家大家有思考过吗,为什么需要内存对齐?为什么要做浪费内存资源的事?直接让数据挨在一起不就行了吗?本篇文章将简单介绍为什么需要内存对齐,以及理解了之后,我们再来看内存对齐。
1. 为什么需要内存对齐
当我们在看一本英语书时,对于熟悉的单词,我们在大脑里面会迅速的反应,并将它翻译为对应的中文含义。但是,当一个单词出现如下情况:
diffe
rence
乍一看,我们会稍微反应一下,然后大脑会反应出,哦原来这是 difference
。当一本书中这种情况多了,就会大大降低我们的阅读效率!
内存大家并不陌生,但是当内存和 CPU
进行数据交换时,一次交换的数据大小是一个字节吗?不是的,通常来说,是以一个字(32位下 4 字节,64位下 8 字节
)来进行数据交换。再结合一段程序(本文都是 32位下
)能更好的说明:
struct stu1
{
char a; // 1
int b; // 4
};
int main()
{
std::cout << sizeof(stu1) << std::endl;
return 0;
}
如果不内存对齐的话,这个程序的输出结果是 5
!内存上是这样的:
一个字长大小为 4
字节,如果 CPU
现在需要成员变量 b
,需要读取两次,第一次读取获取高三个字节,最后一次获取一个字节。但是通过内存对齐后,是这样的:
这只需要读取一次就好啦!
大家会认为内存读取数据的速度很快呀,读取两次和一次差不多吧?对 CPU
来说,简直是天差地别!在他眼里就一直等待… 所以为了加快 CPU
获取数据的速度,使用了内存对齐 — 加快访问速度,提高 CPU 执行效率!利用空间换取时间!
2. 内存对齐的规则
2.1 默认对齐数
当每一次对齐时,我们需要将该数据的大小和默认对齐数进行比较,使用 较小的那一个作为对齐数!
默认对齐数设置方式也很简单,#pragma pack(n)
指令可以将默认的对齐数设置为 n
,其中 n
可以是 1、2、4、8、16
等值,具体取决于编译器的支持。这意味着结构体中的成员将按照 n
字节对齐,而不是编译器默认的对齐方式。
举个栗子:
#pragma pack(8) // 对齐数为 8
struct stu1
{
char a;
int b;
};
int main()
{
std::cout << sizeof(stu1) << std::endl; // 输出结果 8
return 0;
}
-------------------------------------手动分割线
#pragma pack(1) // 对齐数为 1
struct stu1
{
char a;
int b;
};
int main()
{
std::cout << sizeof(stu1) << std::endl; // 输出结果 5
return 0;
}
有些时候,虽然效率是加快了,但是浪费空间的缺点也不容小觑,对于对内存高度紧张的环境,我们就可以适当调节默认对齐数的大小。
2.2 只包含内置类型的场景
在这个场景下结构体中只包含内置类型,默认对齐数为 4
:
struct stu1
{
int a; // 4
char b; // 1
double c; // 8
short d; // 2
long long e; // 8
};
int main()
{
std::cout << sizeof(stu1) << std::endl;
return 0;
}
具体怎么计算的呢,在这里我将介绍我自己使用的方法:
- 从
a
开始,int
为 4 字节,和默认对齐数取小也是 4,从初始地址 1 开始占 4 个字节,现在地址更新到 4; b
,char
为 1 字节,和默认对齐数取小是 1,不需要对齐,从地址 4 开始占 1 个字节,现在地址更新到 5;c
,double
为 8 字节,和默认对齐数取小是 4,需要对齐,到最近往后 4 的整数倍,从地址 8 开始占 8 个字节,现在地址更新到 16;d
,short
为 2 字节,和默认对齐数取小是 2,不需要对齐,从地址 16 开始占 2 个字节,现在地址更新到 20;e
,long long
为 8 字节,和默认对齐数取小是 4,不需要对齐,从地址 20 开始占 8 个字节,现在地址更新到 28;- 最后整个结构体的大小为
最大类型 和 默认对齐数 取小 的整数倍
,最大类型为long long
8 字节,默认对齐数取小是 4;28 已经是 4 的整数倍,所以结果是 28。
我们看看程序的输出:
2.3 内嵌结构体的场景
对于内嵌结构体的场景,我们需要多一项关注 该结构体开始的位置需要是该结构体的最大类型(也要和默认对齐数取小)的整数倍!
#pragma pack(4)
struct stu2
{
int* ptr; // 4
char ch; // 1
};
struct stu1
{
int a; // 4
char b; // 1
stu2 st;
long long c; // 8
};
int main()
{
std::cout << sizeof(stu1) << std::endl;
return 0;
}
在这里我们先计算 stu2
结构体的大小:
- 从
ptr
开始,int*
为 4 字节,和默认对齐数取小也是 4,从初始地址 1 开始占 4 个字节,现在地址更新到 4; ch
,char
为 1 字节,和默认对齐数取小是 1,不需要对齐,从地址 4 开始占 1 个字节,现在地址更新到 5;- 最后整个结构体的大小为
最大类型 和 默认对齐数 取小 的整数倍
,最大类型为int*
4 字节,默认对齐数取小是 4;5 不是 4 的整数倍,所以到最近往后 4 的整数倍 8
所以该结构体的大小就是 8 字节。
现在我们开始计算 stu1
结构体的大小:
- 从
a
开始,int
为 4 字节,和默认对齐数取小也是 4,从初始地址 1 开始占 4 个字节,现在地址更新到 4; b
,char
为 1 字节,和默认对齐数取小是 1,不需要对齐,从地址 4 开始占 1 个字节,现在地址更新到 5;st
,stu2
为 8 字节,该结构体最大类型和默认对齐数取小是 4,需要对齐,到最近往后 4 的整数倍,从地址 8 开始占 8 个字节,现在地址更新到 16;c
,long long
为 8 字节,和默认对齐数取小是 4,不需要对齐,从地址 16 开始占 8 个字节,现在地址更新到 24;- 最后整个结构体的大小为
最大类型 和 默认对齐数 取小 的整数倍
,最大类型为long long
8 字节,默认对齐数取小是 4;24 已经是 4 的整数倍,所以结果是 24。
2.4 类中的其他成员
我们还需要关注在类中的其他成员比如,静态成员函数,成员函数:
struct stu1
{
char a;
int b;
static int c;
void Print() {}
};
int main()
{
std::cout << sizeof(stu1) << std::endl;
return 0;
}
在这里如果不存在后两者,stu1
的大小肯定是 8,加上两者呢?
怎么还是 8 呀?这是因为 成员函数和静态成员变量不独属于任何一个对象,他们属于这个类!sizeof 是计算该对象的大小,自然不算这两者!
2.5 包含虚函数的类
请记住包含虚函数的类,还额外包含一个隐含的成员变量 — 虚函数表指针位于对象首部
,他的对齐方式也很特殊 和类中的最大类型一致!
在这里就不做演示了,大家可以自己尝试计算一下(依旧在 32 位下,但是默认对齐数产生了变化
):
#pragma pack(8)
struct stu1
{
char a;
long long b;
virtual void Print() {}
};
int main()
{
std::cout << sizeof(stu1) << std::endl;
return 0;
}
3. 总结
在这里我们简单说了一下问什么需要内存对齐?以及内存对齐的相应场景,希望大家有所收获!