关于字节对齐问题

我们的讨论从一道经典的题目开始:

Intel和微软同时出现的面试题

#pragma pack(8)

struct s1{
char a;
long b;
};

struct s2{
char c;
s1 d;
long long e;
};

#pragma pack()


1.sizeof(s2) = ?
2.s2的c后面空了几个字节接着是d?

经上机测试,sizeof(s2)=24。s2的C后面空了7个字节。这里涉及到一个我们平时可能会忽视的问题——内存对齐【Memory alignment 】。

1.内存对齐定义:
    现在使用的计算机中内存空间都是按照字节划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际上计算机系统对于基本数据类型在内存中的存放位置都有限制,要求这些数据存储首地址是某个数K的倍数,这样各种基本数据类型在内存冲就是按照一定的规则排列的,而不是一个紧挨着一个排放,这就是内存对齐。

对齐模数
    内存对齐中指定的对齐数值K成为对齐模数(Alignment  Modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。

2.内存对齐的好处:
    内存对齐作为一种强制的要求,第一简化了处理器与内存之间传输系统的设计,第二可以提升读取数据的速度。各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
     Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。但是如果想提升性能,应该注意内存对齐方式。
ANSI  C标准中并没有规定相邻声明的变量在内存中一定要相邻。为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充字节。对于基本数据类型(int  char等),他们占用的内存空间在一个确定硬件系统下有确定的值。ANSI  C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和

3.从80X86结构看内存对齐问题

  3.1 8bit数据总线CPU内存对齐问题

8088和80188的数据总线宽度为8bit,一个内存周期只能读取一个字节。每次读取内存都需要给地址总线提供需要读写内存单元的地址。因此,对于char类型数据, 读取只需要访问一次内存;对于int类型数据,读取需要4次访问内存;所以在这种情况下,没有内存对齐的说法。

  3.2 16bit数据总线CPU内存对齐问题

     8086,80186,80286和80386sx都是16bit数据总线的CPU,这样一个内存周期可以读取16bit数据也就是一个字(word),两个字节的数据。在这种CPU中,内存单元被分成了偶单元(even)和奇单元(odd),如下图所示:

图5 奇单元和偶单元

     16bit数据总线中,0-7bit与偶单元相连,8-15bit与奇单元相连。如下图所示:

图6 16bit数据总线内存访问

     每个内存周期,CPU只能读取一个偶单元和一个奇单元地址总线的地址为偶单元的地址,所以说地址总线的地址永远是2对齐的。

     假如我们读取一个字(Word)数据,该数据是以2对齐的,则一个内存周期即可把它读出开。如果我们读取字(word)数据不是以2对齐的,那么需要读取两个字节周期来完成。比如读取193内存单元中的字,第一个周期中,CPU把地址192放入地址总线,读取其高字节——地址193(16bit数据总线CPU有字节模式)放入地址总线的8-15bit,第二个周期中,CPU把地址194放入地址总线,内存子系统把读取其低字节——地址194,并把其放入地址总线的0-7bit。注意这样一个未对齐字的读取方式不但效率不高,而且得不得想要的结果,CPU会自动的交换0-7bit到8-15bit。

     对于读取一个字节,如果该字节的地址是2对齐的,那么CPU直接把该地址放入地址总线,读取该地址的数据,放入数据总线的0-7bit。如果该字节的地址不是以2对齐的,那么CPU把(该地址-1)的地址放入总线,并读取高字节,放入数据总线的8-15bit,最后CPU自动把8-15bit的数据,交换到0-7bit完成字节的读取。

     对于双字(Double  Word)的访问,如果以2对齐,则只需要2个周期即可完成。如果没有以2对齐,则需要3个内存周期来完成,具体原因可以参考字和字节的分析。

3.3 32bit数据总线的内存访问

    数据总线和内存的连接如下图所示:

图7 32bit数据总线内存访问

     通过上图和参考上面16bit数据总线CPU的分析,可以看出32bit数据总线在每个内存周期中读取的数据地址都是以4对齐的。

    如果读取一个双字的地址是以4内存对齐的话,一个内存周期即可读出。如果不是以4对齐,则需要两个内存周期来完成操作。

    如果读取一个字的地址对4取模余3的话,那么读取该字需要两个内存周期(CPU会自动完成高位和低位的交换)。如果读取一个字的地址对4取模不余3的话,则只需要一个内存周期即可完成读写。

     对于字节,任何字节地址的访问都是一个内存周期。

4.下面我们回到C语言层面讨论这个问题:

4.1在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
     · 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。
     · 使用伪指令#pragma pack  (),取消自定义字节对齐方式。

  另外,还有如下的一种方式:
     · __attribute((aligned  (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
     ·  __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。

4.2内存对齐策略:

     4.2.1 编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。

     4.2.2 成员对齐,即每个成员分别对齐.即每个成员按自己的方式对齐.也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其  类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.(对于指定参数为8时,大于等于4字节的成员的起始位置应该处在4字节的整数倍上,对于2字节成员的起始位置应该处在2字节的整数倍上,1字节的起始位置任意)

    4.2.3结构的长度必须为所用过最大对齐参数的整数倍,不够就补空字节.

程序示例1:

#pragma pack(2)

struct s1{

char a;

long b;

};

cout<

结果:6

程序示例2:

#pragma pack(8)

struct s1{

char a;

long b;

};

cout<

结果:8

你可能感兴趣的:(程序设计)