数据对齐总结
必须注意:对齐是多少字节对齐,不是多少位对齐。
对齐原因:
如上图片,内存一般是四个单位一列,CPU在读取内存数据的时候,通过总线并行读取每个单位的数据。对于CPU 32bit的寄存器而言。
- 0-7bit是来自于内存芯片0的位
- 8-15bit是来自内存芯片1的位
- 16-23bit是来自芯片2
- 24-31来自芯片3
而内存结构是,内存地址一个单位也就是基本单位是1字节,也就是8位二进制数。
在读取数据的时候实际上是0-7位从芯片0读取,8到15位从芯片2读取,以此类推。
所以读取一个4字节数据,是四个芯片同时往寄存器里面读。
如下图:
左边是地址偏移。用于总线高效传输地址,芯片上的数字是内存地址,每个内存地址需要读取几位,就在本芯片上向后读取几位。注意这里说的是物理地址,而不是虚拟地址,物理地址不是线性的,是混乱的。
如果读取4位int,就会从芯片0读取8bits填充寄存器的0--7bit,芯片1读取8bits填充8--15bits,以此类推。
如果数据没有对齐,要从地址1读取4字节,读取进去之后,需要CPU循环左移8bits来达到正确。但是CPU在读取数据的是后需要向内存发送地址,如果没有对齐,发送的地址偏移(左边的地址偏移)量是不同的,1、2、3地址偏移为0,而0芯片接受的地址偏移却为1,这样一来,发送地址就无法使用1根总线来完成了,而是需要4个总线,每个地址总线需要32个CPU引脚,而32bitCPU总共才400多个引脚,这样的设计,增加硬件设计复杂性,是不必要的。如果不改变设计复杂性,访问未对齐数据就需要两次访问才能得到数据。
例如:
访问地址是1、2、3、4的4字节数据,先要读取地址0-3的数据,在读取4--7的数据,再把1--4的数据取出来放入目标寄存器。所以不对其就要多次访问内存。某些CPU比如Power PC直接禁止访问不对其数据。
总结一下为什么需要数据对齐。为了增加CPU读取数据的带宽,内存系统通常都采用并 行结构使得可以并行传输数据。这样的并行结构使得访问对齐的数据速度快,但是若 要使访问不对奇的数据也一样快会使CPU与内存系统的接口变得更复杂,而这是划不来 的。经过权衡之后,最终的结果是:访问对齐的数据速度快,访问不对奇的数据速度 慢(需要2次访问)或干脆禁止访问不对奇数据。
对于基本 类型(不包括结构体,联合体和数组)不同的数据类型的对齐模数通常等于这个数据 类型的大小,比如char的对齐模数为1,short的对齐模 数为2,int的对齐模数为4,float的对齐模数是 4,double的对齐模数为8
当然,这个对齐模数每一个编译器可能有差 异,并且可以设置。在Visual C++中可以通过/Zp选项 获#pragma设置。
综合起来,基本类型的对齐模数是这个类型的大小和 设置的对齐限制的较小者。对于结构体,联合体和数组,对齐模数是它的成员的对齐 模数的最大值。
结构体对齐:
__declspec( align(#) )和#pragma pack( n )
一、#pragma pack(n)
#pragma pack ( 16 )
typedef struct {
int a;
char b;
double c;
}test;
结构体中的数据成员,除了第一个是始终放在最开始的地方,其它数据成员的地址必须是它本身大小或对齐参数两者中较小的一个的倍数。
结构体的数据成员的地址必须是本身大小和对齐参数中较小的那一个。
简单类型组成的结构体,大小由每个成员变量的地址决定。按照定义顺序,分别求出地址开始的地方,从地址0开始,每个变量都采取min(对齐参数,sizeof(type))来确定起始地址是多少的倍数,然后填满该数据,最后求出总大小。在pack大等于2的时候,最终大小要是2的倍数,需要向上取到为2的倍数。pack为1则不需要。
对于含有结构体的结构体,方法同上,结构体变量的对其参数为min(pack对齐参数,结构体成员最大元素大小)。
__declspec( align(#) )
当一个变量或结构体同时受__declspec( align(#) )和#pragma pack( n )影响时,前者的优先级高。
每个成员的地址是前者n的倍数或者后者m的倍数或者是成员大小的倍数。即:
min(n,m,sizeof(成员变量类型));
结构体的最终大小要么是__declspec( align(m) )中m的倍数,要么是结构体中最大类型大小的倍数,去最大。即:
max(m,sizeof(最大类型大小));
结构体中有结构体的例子:
#include <stdio.h> #include "stdafx.h" #include <stdlib.h> //using namespace std; #pragma pack( push, 4 ) __declspec( align(32) )struct D { int i1; double d1; int i2; int i3; }; __declspec( align(16) ) struct E { int i1; D m_d; int i2; }; int main() { cout << "sizeof(int) = "<<sizeof(int) << endl; cout << "sizeof(char) = " << sizeof(char) << endl; cout << "sizeof(double) = " << sizeof(double) << endl; cout << sizeof(D) << endl; cout << sizeof(E) << endl; system("PAUSE"); return 0; }
最后运行的结果是sizeof(E)为96,为何会是这个结果呢?
对于结构体E,第一个元素为int类型,所以占据[0~3]地址单元。
第二个元素是一个结构体,该结构体由于受上面__declspec( align(32) )的影响,优先级高,所以起始地址是32的倍数,而且大小为32B,从而应该放置在[32~63]单元处。
最后一个是int类型的变量,大小为4,所以应该是4的倍数,地址为[64~67]。
故结构体E的大小应该是从[0~67],占据68B,而由于前面还有限制__declspec( align(16) ),同时成员变量的最大偏移是sizeof(D)=32,所以我们最后这个结构体的大小应该是他们中最大值的倍数,也就是32的倍数,68向上取32的倍数应该是96.故结果为96.
注意:_declspec ( align() )的一个特点是,它仅仅规定了数据对齐的位置,而没有规定数据实际占用的内存长度,
当指定的数据被放置在确定的位置之后,其后的数据填充仍然是按照#pragma pack规定的方式填充的。
特别注意:不要忘记__declspec ( align(n) )会让结构体的首地址n字节对齐。