实用经验 14 优化结构体中元素的布局

在开始讨论之前,我们先看两个结构体类型,他们分别为struct A和struct B:

struct A   // A 数据结构1
{
    long lA; // lA long 数据
    char cB; // cB char 数据
    short nC; // nC short数据
};
struct A
{
    char cB;
    long lA;
    short nC;
};

在32位的机器上,char、short、long三种类型的长度分别为1,2,4。在VS2010上测试struct A和struct B的存储长度。sizeof(struct A) = 8,sizeof(struct A) = 12。这也许会让你惊讶,char、short、long分别占用1、2、4个字节。但是按照不同的顺序组合成一个结构体后,结构体的长度会大于三个长度的总和。这就是本实用经验要重点讨论的东西—结构体的内存布局。

结构体元素的布局是结构体定义过程中需要考虑到的。优化结构体元素的布局主要有这两个方面的原因:第一就是节省内存空间,第二就是提供数据存取速度。

说到节省内存空间和提高数据的存取速度。对齐是一个必须讨论的问题。现代计算机中内存空间都是按照byte划分的。理论讲,似乎对任何类型变量的访问都可以从任何地址开始,但是实际情况是在访问特定的变量时经常从特定的存储地址开始访问。这就要求各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个排放。这就是数据的对齐。

然而为什么要进行数据对齐呢?各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为 32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

对齐的实现方式?通常,我们不需要考虑对齐问题。编译器会默认选择目标平台的对齐策略。当然,我们也自己指定数据的对齐方法。

内存对齐
(1)内存数据对齐,降低数据存取的CPU时钟周期;适当的内对齐策略,可降低内存使用量。
(2)内存对齐一般由编译器替我们完成,无特殊需要不需要人为干预。

但是,正因为因为编译器替我们对数据存放做了对齐,但我们并不知道编译器替我们做了这些,所以我们常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果。为此,我们需要对对齐算法有所了解。对齐通常会影响结构体,联合,还有类等复合类型数据的存储区域分布。

一般情况下,对齐算法遵循下述4个规则:

  • 在复合类型中,各数据成员按照他们的声明顺序在内存中顺序存储。第一个成员放到复合类型的起始位置(相对偏移为0)。
  • 每个成员按照自己的对齐方式,并最小化长度。进行自身的数据存储。
  • 复合类型的整体的对齐按照类型中长度最大的数据成员和#progma pack指定值较小的那个值进行对齐。
  • 整个复合类型的长度必须为所采用的对齐参数的整数倍。不够的补空字节。

现在,我们按照上述的对齐准则,再次从理论的高度分析为什么会得到上述的运算结果。首先我们分析 struct A。lA为long类型4字节。lA采用4字节对齐。cB为char型,采用1字节对齐。nC为为short类型采用2字节对齐。整个struct采用4字节对齐。所以可以得出如下结论:la放置于struct A的其实位置,占用4个字节。cB排列到la之后占用一个字节;nC为short型2字节对齐其实地址必须为2的整数倍,所以cB之后必须空闲一个字节,其后面可以存储short 类型的nC。所以struct A在内存中的排列如图2-5所示,按照同样的原理struct B的内存分布,可如图2-6所示。
在这里插入图片描述

图2-5 struct A内存分布图

在这里插入图片描述

图2-6 struct B内存分布图

可以看出,同样的三个数据由于排列顺序不同。导致struct的占用空间发生了很大的变化。所以在定义struct数据类型时,struct中的数据排列顺序是需要重点考虑的。如果在空间紧张时,定义结构体类型时,数据变量的排列顺序应遵守如下的原则:把结构体中变量按照类型大小从小到大的顺序声明,尽量减少中间的空闲填充字节。还有一种就是以空间换取时间,我们显示的填补空间进行对齐,比如:有一种使用空间换时间做法是显式的插入reserved成员:

struct A
{
    char a;
    char reserved[3];  //使用空间换时间
    int b;
}

提示
(1)reserved对程序没有什么意义。它只是填补空间,以达到字节对齐的目的。
(2)当然即使不加入reserved成员,通常编译器也会自动填补对齐。而加上它,只是显式的提醒作用。

接下来,我们看这下面这段代码片段:

unsigned int i = 0x12345678;
unsigned char *p =  NULL;
unsigned short *p1 = NULL;
 p  =  &i;
*p  =  0x00;
 p1 = (unsigned short *)(p+1);
*p1 = 0x0000;

最后两行代码,从奇数边界访问unsignedshort型变量,显然不符合对齐规定。在x86上,这种操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐。

如果你是从事网络协议栈开发的,你可能会为这样的事情烦恼:在定义协议报头时,由于协议报头的这段的顺序是固定的,无法按照类型从小到大的顺序声明,最终导致程序出现奇怪的异常现象。
例如,在小端CPU格式下,IP协议头定义如下:

//  IP头部,总长度20字节
struct IP_HDR
{
    unsigned char ihl:4;        //首部长度
    unsigned char version:4//版本 
    unsigned char tos;			//服务类型
    unsigned short tot_len;		//总长度
    unsigned short id; 		    //标志
    unsigned short frag_off; 	//分片偏移
    unsigned char ttl; 			//生存时间
    unsigned char protocol; 	//协议
    unsigned short chk_sum; 	//检验和
    struct in_addr srcaddr;     //源IP地址
    struct in_addr dstaddr; 	//目的IP地址
} ;

在IP协议头中任何两个逻辑上相邻的字段。都必须在内置中相邻。中间不能出现因为对齐而添加的空闲字段。为了达到取消空闲字段的目的,编译器允许我们自己根据需要设置复合类型(结构体、联合、位段)的对齐方式实现。这就是progma pack()宏。它的功能说明如下:

#pragma pack( [show] | [push | pop] [, identifier], n )

功能说明

  • pack提供数据声明级别的控制,对定义不起作用;
  • 调用pack时不指定参数,n将被设成默认值;
  • 一旦改变数据类型的对齐格式,直接效果就是占用memory的减少,但是性能会下降;

语法说明

  • show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;
  • push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;
  • pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被弹出直到identifier被找到,然后弹出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;
  • identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中弹出所有的record直到identifier被弹出,如果identifier没有被找到,则忽略pop操作;
  • n:可选参数;指定对齐的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。

最后,我们看progma pack()复合类型内存对齐的影响。对比下面三组代码片段,观察同一结构体在不同的对齐方式下,结构体的长度。

片段一:通过progma pack指定对齐格式为1。sizeof(struct A) = 7。

#progma pack(1)
struct A
{
    char b;
    int a;
    short c;
};
progma pack()

片段二:通过progma pack指定对齐格式为2。sizeof(struct A) = 8。

#progma pack(2)
struct A
{
    char b;
    int a;
    short c;
 };
progma pack()

片段三:通过progma pack指定对齐格式为4。sizeof(struct A) = 12。

#progma pack(4)
struct A
{
    char b;
    int a;
    short c;
 };
progma pack()

可以看出,将结果体的对齐方式设置为1,结构体A就不会自动填充空闲字段了。结构体A的长度就是个元素所占字节之和7了。采用某种对齐方式整个结构体的总长度就必须能被对齐方式整除。如对齐方式设置为2,结构体总长度为8可被2整除;对齐方式设置为4,结构体总长度为12可被4整除。

请谨记

  • 掌握复合类型中元素的对齐规则,合理的调整复合类型中元素的布局。这样不仅可以节省空间,还可以提高数据存取效率。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值