1.什么是内存对齐
假设我们同时声明两个变量:
char a;
short b;
用&(取地址符号)观察变量a,
b的地址的话,我们会发现(以16位CPU为例):
如果a的地址是0x0000,那么b的地址将会是0x0002或者是0x0004。
那么就出现这样一个问题:0x0001这个地址没有被使用,那它干什么去了?答案就是它确实没被使用。因为CPU每次都是从以2字节(16位CPU)或是4字节(32位CPU)的整数倍的内存地址中读进数据的。如果变量b的地址是0x0001的话,那么CPU就需要先从0x0000中读取一个short,取它的高8位放入b的低8位,然后再从0x0002中读取下一个short,取它的低8位放入b的高8位中,这样的话,为了获得b的值,CPU需要进行了两次读操作。
但是如果b的地址为0x0002,
那么CPU只需一次读操作就可以获得b的值了。所以编译器为了优化代码,往往会根据变量的大小,将其指定到合适的位置,即称为内存对齐(对变量b做内存对齐,a、b之间的内存被浪费,a并未多占内存)。
2.为什么要内存对齐
当时学C++的时候就没仔细研究过内存对齐这个玩意儿,现在好好看看,终于明白了。
为什么要内存对齐?
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。【这个应该是最主要的】
cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的,块的大小称为(memory granularity)内存读取粒度。
我们再来看看为什么内存不对齐会影响读取速度?
假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:
1.数据从0字节开始
2.数据从1字节开始
解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。
当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,4,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。
但是这还属于乐观情况,上文提到内存对齐的作用之一是平台的移植原因,因为只有部分CPU肯干,其他部分CPU遇到未对齐边界就直接罢工了。
3.例子
(1)
#include<iostream>
using namespace std;
struct A{
char a; // = * * *
int b; // = = = =
short c; // = = * *
};
struct B{
short c; // = =
char a; // = * 这里PPB=4,c和a刚好放在一个内存块
int b; // = = =
};
int main(){
cout<<sizeof(A)<<endl;
cout<<sizeof(B)<<endl;
return 0;
}
以上结构体变量数量类型相同。但是sizeof却不同,
sizeof(A) is 12
sizeof(B) is 8
(2)
#pragma pack(2) //指定PPB为2
struct T{
char a; //偏移地址0
char b; //偏移地址1
int c; //偏移地址2
};
则sizeof(T)=最后一个成员的偏移地址+最后一个成员数的长度=2+4=6。
这里不要看什么偏移,以这题为例,a占1字节,b占1字节,c占4字节
这里PPB为2,所以a和b刚刚好占用一个内存块(PPB=2字节)
即a和b之间不用字节填充,也同样只需要CPU读一次