0%

ANSI-字节对齐

现代计算机的内存空间是以Byte为基本单位划分的.从理论上说,我们可以将任意的数据类型存放在任意的内存空间之上,但实际上,在现代计算机中存在一种叫做字节对齐的机制,以保证数据存放依照一定的规则进行.所以对齐其实和数据在内存中的存放位置有关,如果一个变量的内存地址正好为他的整数倍,则称之为自然对齐.例如在32位系统中,如果一个int类型的地址为0x00000004,则为自然对齐.而我们接下来讨论的字节对齐主要针对自定义类型,例如结构体,联合体等.

字节对齐的作用和优势

字节对齐的作用主要是为了提高内存的访问速度,从而提高程序运行性能.这是因为访问未对齐的内存时,可能需要做两次内存访问,而后做一个内存拼接操作,才可以得到正确的数据.而如果我们按照字节对齐的思路来设计内存,那么我们每次只需要一次内存访问即可得到正确的数据.所以字节对齐的主要含义是牺牲空间来换取时间.

例如如果每次访问要么从0x01-0x04或0x05-0x08,硬件并不支持一次访问就可以从0x02-0x05.这样的话如果我们不对齐字节,是存在将int数据存储到0x02-0x05的可能性,这样的话我们就需要先读取一遍0x01-0x04,保留0x02-0x04;而后再读取一遍0x05-0x08,保留0x05;最后将二者拼接起来,才能得到正确的数据,如此就会降低程序的运行效率.

字节对齐的规则

基本数据类型的自身对齐值

虽然我们所说的字节对齐主要针对自定义类型,但自定义类型实际上是基本数据类型的组合,因此我们需要先了解基本数据类型的自身对齐值,其实也就是基本数据类型的大小.由于不同的编译器和不同的操作系统对于基本数据类型的大小有不同的规定,我们以char为1字节,short为2字节,int为4字节,long为4字节,float为4字节,double为8字节为例进行讨论.

自定义类型的自身对齐值

自定义类型的自身对齐值是指自定义类型(一般指struct,union)中的成员变量的自身对齐值中的最大值.在这样的对齐规则下,自定义类型的空间占用绝不是简单的成员变量的空间大小之和.我们将在后面的例子中给出相关例子以及计算空间的规则.

1
2
3
4
5
6
struct A
{
char a; //1
double b;//1
int c;//1
}

在计算结构体的空间占用时,具体流程如下:

  1. 首先我们需要根据基本类型的自身对齐值来先初步判断结构体的空间占用;
  2. 其次我们按照从上往下对齐的规则来进行第二步的字节对齐,即前面开辟的空间之和是下一个数据类型的整数倍即可,这里的整数倍特指大于空间之和的最小整数倍;
  3. 最后,我们还要保证结构体的整体开辟空间是自身对齐值的整数倍,这样才能保证结构体的整体对齐.

这里其实我们可以发现,虽然我们做的字节对齐绝不是简单的在结构体空间的末尾加上一些空白空间,而是在适当位置加上空白空间,这样才能保证结构体的整体对齐.在上面这个例子中,我们已知char的自身对齐值为1, double的自身对齐值为8, int的自身对齐值为4;char与double对齐时,需要在其后面加上7个空白字节,使得开辟空间是double的整数倍;double与int对齐时,由于double和之前的char一共开辟了16个字节,而int是4个字节,已经是其整数倍,故不需要额外加空白字节;最后由于我们整个结构体开辟了20个字节,但是结构体的自身对齐值是成员变量的自身对齐值中的最大值,即8,所以我们需要在结构体的末尾加上4个空白字节,使得结构体的整体对齐值为8的整数倍.所以最终结构体的空间占用为24个字节.

从这里,我们其实可以意识到,尽管我们定义相同内容的结构体,由于不同的定义顺序,可能会导致结构体的空间占用不同.例如

1
2
3
4
5
6
struct A
{
char a; //1
int b;//1
double c;//1
}

这样的定义方式我们只需要开辟16个字节,因此我们可以看到,结构体的定义顺序是有一定的影响的.如果我们希望节省结构体定义的空间,我们可以在定义时按照基本数据类型的自身对齐值从小到大的顺序进行定义,这样可以减少结构体的空间占用.

程序指定的自身对齐值

在C/C++中,我们可以用#pragma pack(value)来指定程序中的自身对齐值,这样可以改变程序中的自定义类型的自身对齐值.这里如果value=1,则相当于取消对齐.这样的话自定义类型的空间就是成员变量的空间之和.这里设置的value一般而言是要求2的整数次幂,例如1,2,4,8,16等等.如此,我们可以根据实际情况来设置自定义类型的自身对齐值,从而节省空间.
例如:

1
2
3
4
5
6
7
#pragma pack(4)
struct A
{
char a;
double b;
int c;
};

这里由于我们设置了自身对齐值为4,这样的话我们按照上面的推导规则,char与double对齐时,因为我们显式设置了程序的字节对齐值为4,所以我们只需要在其后面补3个空白字节即可;double与int对齐时,因为之前开辟的空间是12个字节,int是4个字节,所以我们不需要补空白字节;最后我们看到整个结构体的空间占用为16个字节,这样他是我们设置的自身对齐值的整数倍,所以我们不需要再补空白字节.

值得注意的是,我们设置的自身对齐值是可能与前面提到的自定义类型的自身对齐值有冲突的,这时候,我们就需要讨论后续的对齐规则,也就是所谓的有效对齐值.

自定义类型的有效对齐值

自定义类型的有效对齐值是指自定义类型的自身对齐值和程序指定的自身对齐值中的最小值.这样的话,我们可以根据有效对齐值来计算自定义类型的空间占用.例如

1
2
3
4
5
6
7
#pragma pack(4)
struct A
{
char a;
short b;
double c;
};

按照上面的规则,我们逐步分析,char与short对齐时,虽然我们设置了程序的字节对齐值为4,但是short的自身对齐值为2,所以我们只需要按照2字节对齐即可,所以我们只需要在char后面补一个空白字节即可;在short与double对齐时,因为之前开辟的空间是4个字节,而double的自身对齐值为8,但我们设置了程序的字节对齐值为4,所以我们不需要做什么操作;最后我们看到整个结构体的空间占用为12个字节,这样他是有效对齐值4的整数倍.

额外的情况

联合体的字节对齐

上面我们讨论的都是结构体的字节对齐,但是联合体其实与之十分类似,但是由于结构体是为每个成员开辟了独立的空间,但是联合体则是所有成员共用一个空间,因此二者的空间占用计算是不同的.

1
2
3
4
5
6
union A
{
char a[10];
short b;
double c;
};

在这个例子中,我们可以意识到,如果我们分别为不同的成员开辟空间,那么我们需要为char[10]开辟10个字节,为short开辟2个字节,double开辟8个字节,根据联合体的定义,我们只需要开辟最大的成员的空间,即10个字节;但值得注意的是,联合体的空间占用也是需要满足字节对齐的,联合体的自身对齐值是其成员的自身对齐值的最大值,也就是8.同样我们也需要要求联合体整体空间是自身对齐值的整数倍,因此我们只需要在联合体的末尾加上6个空白字节即可,使得联合体的整体对齐值为8的整数倍.

这里我们要强调的是,我们所说的自身对齐值指针对变量类型,而不是变量的大小,就像我们这个例子所提到的char[10],他的对齐值是1,而不是10.

内嵌结构体的字节对齐

在实际使用中,我们可能会出现需要在struct中嵌套struct的情况.我们按照上面的思想对其进行分析,

1
2
3
4
5
6
7
8
9
10
11
struct A
{
short a;
struct
{
double b[10];
int c;
char d;
}
long e;
};

我们先从内部的struct分析,double b[10]需要开辟80个字节,但其的对齐值是8.double和int对齐时,double[10]开辟出的80个空间是int的整数倍,所以我们不需要额外补充空白字符;int与char对齐时,int以前开辟的空间为84个字节,是char的整数倍.因此不需要开辟空白字节;但是由于内部的struct的自身对齐值为8,所以我们需要在末尾补上3个字符,使得整体开辟88个字节,符合struct的自身对齐值的整数倍.但是值得注意的是,如果我们分析struct A的时候,内部struct的对齐值并不是88,而是其成员类型的最大对齐值.所以我们可以开始分析struct A.short与内部struct对齐时,short的字节空间为2,并不是内部struct的整数倍,因此我们需要在short后面补6个空白字节,使得short的空间为8的整数倍;内部struct与long对齐时,内部struct的空间以前开辟的空间为96,是long的整数倍,所以我们不需要额外开辟空白字节.最后我们看到整个struct A的空间占用104个字节,这样他是struct A的自身对齐值的整数倍.

但是这里我们需要注意的是,如果我们在内部struct给了一个类型命名,那么struct A的字节占用会有一个很大的变化,如

1
2
3
4
5
6
7
8
9
10
11
struct A
{
short a;
struct t
{
double b[10];
int c;
char d;
}
long e;
};

这样的话,我们会认为struct t是一个内嵌类型,并不会为只分配空间,所以也就是说,struct t的类型定义和typedef的作用是一样的,并不会为其分配空间.所以我们在分析struct A的时候,只需要按照除了内部struct以外的成员变量来分析即可.但是值得注意的是,这里如果我们定义了变量的话,那么就需要考虑空间占用.

其次我们上面讨论的内嵌类型只针对C++,在C中并没有这样的概念,所以在C中,我们需要为内嵌类型分配空间.