[C++] 内存对齐的目的和原则是什么?如何调整内存对齐的值
为什么需要内存对齐?内存对齐(memory alignment).为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。也就是说“内存对齐”应该是编译器的管辖范围,非常依赖平台.全局关闭内存对齐先来看下面的结构体:struct test{int a;int b;char c;}A;1 添加
1. 为什么需要内存对齐?
内存物理结构
我们来了解一下内存的物理构造,一般内存的外形图片如下图:
一个内存是由若干个黑色的内存颗粒构成的。每一个内存颗粒叫做一个chip。每个chip内部,是由8个bank组成的。其构造如下图:
而每一个bank是一个二维平面上的矩阵,前面文章中我们说到过。矩阵中每一个元素中都是保存了1个字节,也就是8个bit。
内存编址方式
那么对于我们在应用程序中内存中地址连续的8个字节,例如0x0000-0x0007,是从位于bank上的呢?直观感觉,应该是在第一个bank上吗? 其实不是的,程序员视角看起来连续的地址0x0000-0x0007,实际上位8个bank中的,每一个bank只保存了一个字节。在物理上,他们并不连续。下图很好地阐述了实际情况。
你可能想知道这是为什么,原因是电路工作效率。内存中的8个bank是可以并行工作的。 如果你想读取址0x0000-0x0007,每个bank工作一次,拼起来就是你要的数据,IO效率会比较高。但要存在一个bank里,那这个bank只能自己干活。只能串行进行读取,需要读8次,这样速度会慢很多。
结论
所以,内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的。 对于64位数据宽度的内存,假如cpu也是64位的cpu(现在的计算机基本都是这样的),每次内存IO获取数据都是从同行同列的8个chip中各自读取一个字节拼起来的。从内存的0地址开始,0-7字节的数据可以一次IO读取出来,8-15字节的数据也可以一次读取出来。
换个例子,假如你指定要获取的是0x0001-0x0008,也是8字节,但是不是0开头的,内存需要怎么工作呢?没有好办法,内存只好先工作一次把0x0000-0x0007取出来,然后再把0x0008-0x0015取出来,把两次的结果都返回给你。 CPU和内存IO的硬件限制导致没办法一次跨在两个数据宽度中间进行IO。这样你的应用程序就会变慢,算是计算机因为你不懂内存对齐而给你的一点点惩罚。
引入内存对齐的原因
- 硬件取指的方便
- 在于移植性的要求
2. 内存对齐的原则
原则1:数据成员对齐规则:结构(struct或联合union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员obj存储的起始位置要从该成员obj大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储,short是2字节,就要从2的整数倍开始存储)。
原则2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。)
原则3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。如果是结构体B包含了结构体A对象a,判断最大成员时并不是a,而是a结构体的最大成员。
(补:上述取最大成员的大小后,实际上应该取[#pragma pack指定的数值]与[最大成员的数值]比较小的那个为准)
这三个原则具体怎样理解呢?我们看下面几个例子,通过实例来加深理解。
例1:
struct{
short a1;
short a2;
short a3;
}A;
struct{
long a1;
short a2;
}B;
sizeof(A) = 6; 这个很好理解,三个short都为2。
sizeof(B) = 8; 这个比是不是比预想的大2个字节?long为4,short为2,整个为8,因为原则3。
例2:
struct A{
int a;
char b;
short c;
};
struct B{
char b;
int a;
short c;
};
sizeof(A) = 8; int为4,char为1,short为2,这里用到了原则1和原则3。
sizeof(B) = 12; 是否超出预想范围?char为1,int为4,short为2,怎么会是12?还是原则1和原则3。
深究一下,为什么是这样,我们可以看看内存里的布局情况。
A的内存布局:
a b c
1111, 1*, 11
B的内存布局:
b a c
1***, 1111, 11**
其中星号*表示填充的字节。
A中,b后面为何要补充一个字节?因为c为short,其起始位置要为2的倍数,就是原则1。c的后面没有补充,因为b和c正好占用4个字节,整个A占用空间为4的倍数,也就是最大成员int类型的倍数,所以不用补充。
B中,b是char为1,b后面补充了3个字节,因为a是int为4,根据原则1,起始位置要为4的倍数,所以b后面要补充3个字节。c后面补充两个字节,根据原则3,整个B占用空间要为4的倍数,c后面不补充,整个B的空间为10,不符,所以要补充2个字节。
再看两个结构中含有结构成员的例子:
例3:
struct A{
int a;
double b;
float c;
};
struct B{
char e[2];
int f;
double g;
short h;
struct A i;
};
sizeof(A) = 24; 这个比较好理解,int为4,double为8,float为4,总长为8的倍数,补齐,所以整个A为24。
sizeof(B) = 48; 看看B的内存布局。
B的内存布局
e f g h i
11**,1111, 11111111,11******,1111****, 11111111, 1111****
例4:
struct A{
int m1;
int *m2;
}a;
struct B{
int m1;
struct A m2;
}b;
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(b));
输出16 24。
原则2,A的存储位置将由8开始,所以结构体B的成员b.m2.m1并不会与b.m1同存储于前八个字节中;
原则3,结构体的总大小并不是取决于结构体A的大小,而是A结构体的最大成员,所以总大小是24而不是32。
设计结构体习惯
最后顺便提一点,在设计结构体的时候,一般会遵照一个习惯,就是把占用空间小的类型排在前面,占用空间大的类型排在后面,这样可以相对节约一些对齐空间。
全局关闭内存对齐
先来看下面的结构体:
struct test{
int a;
int b;
char c;
}A;
1 添加预处理指令 #pragma pack(1)
#paragma pack(1)预处理指令的作用是结构体在分配内存时按一个字节对齐。
#include<stdio.h>
#pragma pack(1)
struct test{
int a;
int b;
char c;
}A;
int main()
{
printf("sizeof(A) = %d\n",sizeof(A));
return 0;
}
结果为:
$ ./a.out
sizeof(A) = 9
实际上,如果用了#paragma pack(1)预处理指令,则整个文件中都会关闭内存对齐,如果只想对其中某个或某几个结构其关闭呢,这是就要用到第二种方法了。
选择性关闭内存对齐
利用__attribute__ ((packed))指令可以选择性的对内存对齐进行关闭。
#include<stdio.h>
#pragma pack(push)
#pragma pack(1)
struct testA{
int a;
int b;
char c;
}A;
#pragma pack(pop)
struct testB{
int a;
int b;
char c;
}B;
void main()
{
printf("sizeof(A) = %d\n",sizeof(A));
printf("sizeof(B) = %d\n",sizeof(B));
}
$ ./a.out
9
12
参考文章:
1.https://zhuanlan.zhihu.com/p/83449008
更多推荐
所有评论(0)