1. 什么是字节对齐
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”。比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。
2. 字节对齐的原因
移植原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
性能原因:提高CPU访问数据的效率。为了访问未对齐的内存,CPU需要作多次内存访问;而对齐的内存访问仅需要一次访问。
3. 字节对齐的原则
字节对齐与编译器的实现有关,但一般而言,需要满足三个原则:
<1> 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
<2> 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal padding)。
<3> 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。
附:
CentOS5.5 x86_64 gcc4.1.2环境下C语言基本数据类型的长度(byte) | |
char | 1 |
int | 4 |
float | 4 |
double | 8 |
short int | 2 |
long int | 8 |
long long int | 8 |
long double | 16 |
void | 1 |
void * | 8 |
char *p | 8 |
4. 结构体联合对齐
编译器默认情况下会对结构体作边界对齐。系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置(即其宽度的整数倍)。
struct ALIGN
{
char a;
int b;
char c;
};
a | internal |
| padding | b (4byte) | c | trailing |
| pading |
* ALIGN结构体中,边界要求最严格的数据类型是int,即4byte,所以其a成员必须存储于一个能够被4整除的地址。
* 成员b相对结构首地址的偏移量是其大小的整数倍,所以b应存储于a之后跳过3个字节的位置。成员c紧随b之后便可。
* 结构体的总大小为结构体最宽基本类型成员大小的整数倍,所以在c之后需要填充3个字节。
从ALIGN结构体的存储布局来看,该结构体占用12个字节的内存空间,但实际只使用其中的6个,损失了近一半的空间。为了最大限度地减少因边界对齐而带来的空间损失,我们可以在声明中对结构体的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。如:
struct ALIGN
{
int b;
char a;
char c;
};
b (8byte) | a (1byte) | c (1byte) |
|
|
结构的成员应该根据它们的边界需要进行重排,以减少因边界对齐而造成的内存损失。即使,我们可能想把相关的结构成员存储在一起,提高程序的可维护性和可读性。当程序将创建几百个甚至几千个结构时,减少内存浪费的要求就远比可读性重要。为避免可读性方面的损失,可在声明中添加注释。
1>结构成员均为基本数据类型
struct VARIABL
{
char a;
long int b;
short int c;
char d[10];
};
a |
|
|
|
|
|
|
| b (8byte) | c (2) | d.0 | . | . | . | . | . | . | . | . | d.9 |
|
|
|
|
sizeof (struct VARIBLE)的值为32。
成员a的起始地址为最宽数据类型long的整数倍。b相对结构首地址的偏移量是其大小8的整数倍,故而在a跳过7个字节的位置。c相对结构首地址的偏移量是其大小2的整数倍,故而紧接b后面。d为字符类型数组,单个大小为1,故而可紧接c后面。因整个结构体大小是最宽类型long即8bytes的整数倍,所以需要在末尾添加4个bytes。
2>结构成员包含结构体
struct S1
{
short int a; /*2bytes*/
long int b; /*8bytes*/
};
struct S2
{
char c; /*1byte*/
struct S1 d; /*16bytes*/
long double e; /*16bytes*/
};
sizeof(struct S1)的值为16,sizeof(struct S2)的值为48。
struct S1的内存空间布局:
a 2byte |
|
|
|
|
|
| b 8byte |
结构体S1中,long类型为8字节,根据字节对齐的三个原则容易得出上述布局及sizeof(struct S1)的大小。
struct S2的内存空间布局:
0 8 16 24 32
c |
|
|
|
|
|
|
| d.a |
|
|
|
|
|
|
| d.b |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| e 16byte |
结构体S2中,long double类型的宽度为16bytes即最大。所以结构的起始地址必须是16的整数倍。又因为成员d也是一个结构体,其自身也遵循字节对齐的三个原则,即结构体变量d的存储地址必须是其内部最宽成员长度8的整数倍,所以d的起始地址在成员c跳过7个字节后的位置。而成员e的长度为16bytes,其存储地址也必须是结构体S2起始地址偏移16的整数倍的位置,如上图,需要跳过24到32之间的字节的位置。
3>结构成员包含联合
struct VARIABLE
{
enum {INT, FLOAT, STRING} type; /*4bytes*/
union
{
int i;
float f;
char *s;
}value; /*8bytes*/
};
sizeof (struct VARIABLE)的值为16。
struct VARIABLE的内存空间布局:
type 4byte |
|
|
|
| value 8byte |
枚举变量的大小与int类型变量相同。
联合的所有成员引用的是内存中的相同位置。该内存空间要大到足够容纳最宽的成员,并且,其对齐方式要适合于所有类型的成员。对联合允许的操作与对结构体允许的操作相同:作为一个整体单元进行赋值、复制、取地址及访问其中一个成员。联合只能用其第一个成员类型的值进行初始化。
联合变量value中最宽的成员是字符指针s,其长度为8.所以value的大小为8bytes。再根据字节对齐的三个原则,可知sizeof (struct VARIABLE)的值为16.
4>联合成员包含结构
typedef long Align;
union header
{
struct
{
union header *ptr; /*空闲块链表中的下一块*/ /*8bytes*/
unsigned int size; /*本块的大小*/ /*4bytes*/
}s;
Align x; /*强制块的对齐*/ /*8bytes*/
};
sizeof (union header)的值为16.
union header的内存空间布局:
ptr 8byte | size 4byte |
|
|
|
|
或
x 8byte |
|
|
|
|
|
|
|
|
联合变量的大小是其最宽成员的大小。union header联合有两个成员,一个是结构体变量s,其大小为16bytes。另一个是长整型变量x,其大小为8bytes。所以该联合的大小为16bytes。
5. 编译器对齐方式
x86下,GCC默认按4字节对齐;x86_64环境下,GCC默认按8字节对齐。
修改GCC对齐大小可通过2种方法:
1> C预处理命令#pragram pack
主要用于改变C编译器的字节对齐方式。
使用方法:
#pragram pack(n) /*编译器将按照n个字节对齐*/ /*对齐边界n必须是2的较小次方:1、2、4、8、16*/
#pragram pack() /*取消自定义字节对齐方式*/
使用规则:
如果#pragma pack(n)中指定的n大于结构体中最大成员的size,则其不起作用,结构体仍然按照size最大的成员进行对界。
#pragram pack(n) /*按n字节对齐*/
struct ALIGN
{
char a; /*1byte */
long b; /*8bytes*/
char c; /*1byte */
};
#pragram pack() /*取消对齐*/
n的值 | 1 | 2 | 4 | 8 | 16 | 没有#pragma |
sizoef(struct ALIGN)的值 | 10 | 12 | 16 | 24 | 24 | 24 |
当n==1时,内存空间布局为如下。此时,成员是按1字节来对齐的,成员a之后就紧接着b,接着就是c。
a 1byte | b (8byte) | c 1byte |
当n==2时,内存空间布局为如下。此时,成员是按2字节来对齐的,成员a之后跳过1个字节就是b,接着就是c,c后面填充1个字节。
a |
| b 8byte | c |
|
2> GNU C中的__attribute__机制
__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute )
__attribute__((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。n为2的某次方。
__attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
struct ALIGN
{
char a; /*1byte */
long b; /*8bytes*/
char c; /*1byte */
}__attribute__((packed));
此时,结构体struct ALIGN按照实际占用字节数对齐。故而,sizeof (struct ALIGN)的值为10.
struct ALIGN
{
char a; /*1byte */
long b; /*8bytes*/
char c; /*1byte */
}__attribute__((aligned(n)));
n的值 | 1 | 2 | 4 | 8 | 16 | 32 |
sizoef(struct ALIGN)的值 | 24 | 24 | 24 | 24 | 32 | 32 |
当n为1、2、4时,比最大宽度成员b的值8还小,所以结构体按照8的长度来对齐。
当n==16或n==32时,内存空间布局如下。结构成员a对齐在n整数倍的位置上,成员b的长度为8小于n,所以其与结构首地址的偏移量大小为其自身,c则可紧随其后。
a |
|
|
|
|
|
|
| b 8byte | c |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6. 字节取消对齐的影响及应用
内存空间都是按照自己划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际上计算机系统对于基本数据类型在内存中的存放位置都有限制,要求这些数据存储首地址是某个数K的倍数,这样各种基本数据类型在内存中就是按照一定的规则排列的,而不是一个紧挨着一个存放,这就是内存对齐。
一般情况下,对数据的存储不按字节对齐来,那就会影响CPU读取数据的效率。如,一个32位整型,存放在奇地址开始的地方,就需要2个读周期并对两次读出的结果的高低字节进行拼凑才能得到该32位的数据。
Intel的IA32架构的处理器则不管数据是否对齐都能正确工作,但如果内存字节对齐了则能提高CPU的性能。
既然字节对齐能提高CPU读数据的效率,那为何我们要改变编译器的默认对齐,甚至让其按照实际占用字节数来对齐呢?如使用__attribute__((packed))。或者使用该GCC编译器的属性有什么好处,什么情况下需要应用到它呢?
先看下GCC手册的说明。
再看结构体非对齐的结构例子:
/* 写到物理空间的管理头部信息*/
typedef struct head_persist
{
unsigned char ch;
unsigned int check_sum; /*校验和*/
unsigned int data_num; /*数据个数*/
unsigned int finish_data; /*已经完成的数据个数*/
}__attribute__ ((packed))head_persist_t;
结论:当我们需要把内存上的结构数据写到磁盘上时,如果采用结构体对齐的方式,可能中间包含一些我们不需要的0,这样当我们借助其他工具查看物理空间上的数值时,需要跳过那些填充的0,可能造成查看不便;而使用按照实际占用字节数来进行对齐的话,查看物理空间上的数值值就很方便了。
__attribute__ ((packed))更多的是在跨平台通信时用到,因不同平台内存对齐方式可能不同。这样当使用结构体进行通信时,如果不使用__attribute__ ((packed))则可能会产生问题。