一 什么是字节对齐
现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。
二 对齐的原因和作用
2.1、硬件平台
不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放。例如Motorola 68000 处理器不允许16位的字存放在奇地址,否则会触发异常,因此在这种架构下编程必须保证字节对齐。
2.2、效率问题
如果不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。比如32位的Intel处理器通过总线访问(包括读和写)内存数据。每个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。如果一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就需要2个总线周期对其进行访问,显然访问效率下降很多。
2.3、节省空间
合理利用字节对齐还可以有效地节省存储空间,后面详细解释原因。
2.4、注意事项
合理利用字节对齐还可以有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会降低变量访问速度。因此需要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。
三、对齐方式
基础知识:
32位,X86处理器,GCC编译器各数据类型的长度为:
char为1字节、short为2字节、int为4字节、long为4字节、float为4字节、double为8字节。
3.1、对齐规则
结构体字节对齐的细节和具体编译器实现相关,但一般而言满足几个规则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
3) 结构体的总大小为结构体最宽基本类型成员大小(有pack时,看规则5,最小的有效对齐值)的整数倍,如有需要编译器会在最末一个成员之后加上填充字节{trailing padding}。
4) 指定对齐值:#pragma pack (value)时的指定对齐值value。
5) 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。
规则详解:
1)编译器给结构体开辟空间时,首先找到结构体里最宽的基本数据类型,然后开辟的内存地址能够被该基本数据类型整除,才可以作为结构体首地址,最宽的基本数据类型就是自身对齐值。
2)在开辟结构体下一个成员之前,要检查预开辟的空间地址的首地址是否是本成员大小的整数倍,若是,则存放本成员;反之,则在本成员和上一个成员之间条虫一定的字节,已达到整数倍的要求。
3)结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条( 结构体的总大小为结构体最宽基本类型成员大小的整数倍),否则就必须在最后填充几个字节以达到本条要求。
4)指定对齐值,不会根据结构体成员规定对齐值。
5)实际上是对规则1、4的情况冲突的解决方法;同样的规则3,也需要在规则5的基础上。
案例1:
struct A{
int a;
char b;
short c;
};
1)假设A的地址是0x0000,则a的地址是0x0000~0x0003;
2)b的地址是0x0004;
3)c的地址往后排是0x0005,但0x0005%2 != 0,根据规则2,填充一个字节,c应该是0x0006~0x0007;
4)则sizeof(strcut A) = 8,同时满足规则3,8%4 = 0(默认的是四字节对齐)。
案例二:
struct B{
char b;
int a;
short c;
};
1)假设B的地址是0x0000,则b的地址是0x0000;
2)a的地址往后排是0x0001,但0x0001%2 != 0,根据规则2,填充三个字节,a应该是0x0004~0x0007;
3)c的地址往后排是0x0008,0x0008%2 = 0,c应该是0x0008~0x0009;
4)现在sizeof(strcut A) = 10,但不满足规则3,10%4 != 0(默认的是四字节对齐),需要往后填充两个字节;
5)则sizeof(strcut A) = 12。
3.2、对齐方式修改 (pack)
1、伪指令
- 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
- 使用伪指令#pragma pack(): 取消自定义字节对齐方式。
案例一:
#pragma pack(2) //指定按2字节对齐
struct C{
char b;
int a;
short c;
};
#pragma pack() //取消指定对齐,恢复缺省对齐
1)变量b自身对齐值为1,指定对齐值为2,所以有效对齐值为1。
2)假设C从0x0000开始,则b存放在0x0000,符合0x0000%1= 0;
3)变量a自身对齐值为4,指定对齐值为2,所以有效对齐值为2,本来应存放在0x0001~0x0004四个字节中,但不符合规则2,因此需要填充1字节, 然后存放在0x0002~0x0005四个字节中,符合0x0002%2=0。
4)变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006~0x0007中,符合 0x0006%2=0。
5)所以从0x0000到0x00007共八字节存放的是C的变量。C的自身对齐值为4,所以其有效对齐值为2。又8%2=0,C只占用0x0000~0x0007的八个字节。所以sizeof(struct C) = 8。
案例二:
#pragma pack(8)
struct D{
char b;
short a;
char c;
};
#pragma pack()
1)#pragma pack(8),但依然按照两字节对齐,原因:对齐到的字节数 = min{当前指定的pack值,最大成员大小}。
2)所以sizeof(struct D)的值为6。分析同上。
2、GCC特有语法:
- __attribute((aligned (n))): 让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。
- __attribute__ ((packed)): 取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
GNU GCC编译器中按1字节对齐可写为以下形式:
#define GNUC_PACKED __attribute__((packed))
struct C{
char b;
int a;
short c;
}GNUC_PACKED;
sizeof(struct C)的值为7。
四、使用字节对齐优化内存
我们在不同编译平台或处理器上,字节对齐会造成消息结构长度的变化。编译器为了使字节对齐可能会对消息结构体进行填充,不同编译平台可能填充为不同的形式,大大增加处理器间数据通信的风险。
为了尽量避免这种情况,同时也优化内存单元,提出下面几种方案:
1)为了提高内存访问效率,采用四字节对齐方式;
2)对于不同处理器之间的结构,减少编译平台带来的变化,保证内存访问效率,对结构体成员采用字节填充的方法,自己对成员进行四字节对齐;
3)兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排原则是:四字节的放在最前面,四字节放完之后,两字节的紧跟,两字节放完之后,一字节紧跟,最后放填充字节。
案例:
typedef struct MSG{
long ParaA;
long ParaB;
short ParaC;
char ParaD;
char NO; //填充字节
}T_MSG;