数据对齐
程序中变量的存储可以直接影响到程序的运行速度。计算机中都是以字节划分内存空间,通常编译器会为我们选择适合目标平台的对齐策略,但是有时候也带了一些麻烦,要自定义变量的对齐策略我们就需要用到#pragma pack
。
应用场景
百度百科里提到一个典型的应用场景:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。
说白了,一个正确的字节对齐方式,就是为了让CPU在最短的时间内读取完变量,同时还让整体的结构存储空间最小。
字节对齐方式的使用
四个概念
- 数据类型自身的对齐值:基本数据类型的自身对齐值。
- 结构体或者类的自身对齐值:其数据成员中自身对齐值最大的那个值。
- 指定对齐值:系统默认的或者我们通过
#pragma pack(value)
指定对齐值value。 - 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
想要巧妙设定字节对齐的字节数,必须要知道上面四个数值之间的关系:数据成员、结构体和类的有效对齐值,是最终存储时的数据、结构体和类决定数据存放地址方式的值。分为数据有效对齐值,结构体和类有效对齐值两个。同时整体的要满足这四条规则。
- 数据有效对齐值 = min(数据类型的自身对齐值,指定对齐值value)。
- 结构体,联合体和类有效对齐值 = min(结构体、联合体和类自身对齐值,指定对齐值value)
- 保证每一个数据满足:不同类型数据都满足:当前数据存储的首地址 % 数据有效对齐值 = 0;
- 补齐末尾的空间,保证长度对齐,即总长度 % 实际对齐长度 = 0;
选择最好的对齐字节数值
最好的value的值,就是和变量自身对齐值为整数数关系下,使得总体数据结构占用空间最小的值。
//默认字节对齐方式为4字节对齐
struct C {
char b; //自身对齐值为1,有效对齐值为min(1,4)
int a; //自身对齐值为4,有效对齐值为min(4,4)
short c; //自身对齐值为2,有效对齐值为min(2,4)
};
//由上述第2条:C的有效对齐值为4。
数据结构大小计算:
- 我们认为第一个元素的起始地址为0x0000,所以,第一个
char b
的有效对齐值为1,0x0000%1=0,符合。 - 第二个元素
int a
,有效对齐值为4,初始位置为0x0001,0x0001%4=1,不符合,往后面移动一位,0x0002%4=2,不符合,到0x0004,0x0004%4=0,符合,所以int a
的起始地址为0x0004。 - 第三个元素
short c
,有效对齐值为2,起始地址为0x0008,0x0008%2=0,符合,所以short c
起始地址为0x0008 - 所以结构体C的整体存储空间大小为:4+4+2=10,10%4=2,所以要满足上面的第四条,则0x0010和0x0011也是C的空间,这样C一共就占了12个字节。
但是明显这个不是最优的字节对齐方式,看看下面这个:
//改变字节对齐方式为2字节对齐
struct C {
char b; //自身对齐值为1,有效对齐值为min(1,2)
int a; //自身对齐值为4,有效对齐值为min(4,2)
short c; //自身对齐值为2,有效对齐值为min(2,2)
};
//由上述第2条:C的有效对齐值为2。
数据结构大小计算:
- 第一个元素的起始地址为0x0000,
char b
的有效对齐值为1,0x0000%1=0,符合。 - 第二个元素
int a
,有效对齐值为2,初始位置为0x0001,0x0001%2=1,不符合,往后面移动一位,0x0002%2=0,符合,所以int a
的起始地址为0x0002。 - 第三个元素
short c
,有效对齐值为2,起始地址为0x0006,0x0006%2=0,符合,所以short c
起始地址为0x0006。 - 所以结构体C的整体存储空间大小为:2+4+2=8,8%2=0,所以满足上面的第四条,这样C一共就占了8个字节。
所以,设置不同的字节对齐方式对于数据的存储空间来说有不同的影响,在和变量自身对齐值为整数数关系下,选择变量有效对齐值中位数附近的数值效果最好。
#pragma pack
作用:
指定结构体,联合,类成员的字节对齐方式
语法:
#pragma pack( show | push | pop ,identifier, n )
show
:#pragma pack(show)
:显示当前字节对齐方式的字节数,以warning显示。
push
:#pragma pack(push)
:将当前的字节对齐方式入栈(internal compiler stack),也就是保存当前的字节对齐方式。
pop
:#pragma pack(pop)
:将(internal compiler stack)栈顶的记录出栈,也就是恢复最新的一次字节对齐方式。
identifier
:#pragma pack(push,identfier)
:当同push一起使用时,赋予当前被压入栈中的record一个名称;#pragma pack(pop,identfier)
:当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作。
n
:#pragma pack(n)
:指定n
个字节对齐,合法数值为2的非负整数次幂: 1、2、4、8、16,缺省时为8。#pragma pack(push,n)
:当同push一起使用时,将当前的字节对齐方式入栈,同时设置当前的字节对齐数值为n
。#pragma pack(pop,n)
:当同pop一起使用时,将栈顶的字节对齐方式出栈,同时设置当前的字节对齐数值为n
。
使用用例
设置字节对齐方式为2字节对齐:
#pragma pack(2)
struct C {
char b; //自身对齐值为1,有效对齐值为min(1,2)
int a; //自身对齐值为4,有效对齐值为min(4,2)
short c; //自身对齐值为2,有效对齐值为min(2,2)
};
#pragma pack()//恢复默认字节对齐方式