内存对齐

Keep Open and Learning
Post Reply
星际浪子
Posts: 3597
Joined: 01 May 2009 23:45

内存对齐

Post by 星际浪子 » 05 Aug 2011 17:39

  C++编程语言对内存的操作是一个非常复杂的应用步骤,我们在学习的过程中,需要不断的从实践编程中去总结这方面的应用经验,以帮助我们理解。在这里我们就先来介绍一下C++内存对齐的相关方法。
  一、为什么会有C++内存对齐
  以下内容节选自《Intel Architecture 32 Manual》。
  为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
  一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
  二、C++内存对齐规则
  每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
  对齐规则:
  1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照 #pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
  2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
  3、结合1、2推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
  4.各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。
  5.各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节自动填充。
  6.同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
  三、pragma pack 宏
  VC中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。下面举例说明其用法。
  #pragma pack(push) //保存对齐状态 #pragma pack(4)//设定为4字节对齐 struct test { char m1; double m4; int m3; }; #pragma pack(pop)//恢复对齐状态
  以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节。接着为m3分配空间,这时其偏移量为 12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragma pack(4)改为#pragma pack(16),那么我们可以得到结构的大小为24。

星际浪子
Posts: 3597
Joined: 01 May 2009 23:45

Re: 内存对齐

Post by 星际浪子 » 05 Aug 2011 17:58

结构体数据成员对齐的意义
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的起始地址的值是某个数k的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。
结构体对齐包括两个方面的内容
1. 结构体总长度
2. 结构体内各数据成员的内存对齐,即该数据成员相对结构体的起始位置
结构体大小的计算方法和步骤
1. 将结构体内所有数据成员的长度值相加,记为sum_a;
2. 将各数据成员为了内存对齐,按各自对齐模数而填充的字节数累加到和sum_a上,记为sum_b。对齐模数是#pragma pack指定的数值以及该数据成员自身长度中数值较小者。该数据相对起始位置应该是对齐模式的整数倍。
3. 将和sum_b向结构体模数对齐,该模数是#pragma pac指定的数值和结构体内部最大的基本数据类型成员长度中数值较小者。结构体的长度应该是该模数的整数倍。
结构体大小计算举例
在计算之前,我们首先需要明确的是各个数据成员的对齐模数,对齐模数和数据成员本身的长度以及pragma pack编译参数有关,其值是二者中最小数。如果程序没有明确指出,就需要知道编译器默认的对齐模数值。下表是Windows XP/DEV-C++和Linux/GCC中基本数据类型的长度和默认对齐模数。
char short int long double long double
Windows 长度 1 2 4 4 8 12
模数 1 2 4 4 8 4
Linux 长度 1 2 4 4 8 12
模数 1 2 4 4 4 4
例子1:
struct my_struct
{
char a;
long double b;
};
此例子Windows和Linux计算方法一样,如下:
步骤1:所有数据成员自身长度和:1B + 12B = 13B,sum_a = 13B
步骤2:数据成员a放在相对偏移0处,之前不需要填充字节;数据成员b为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是4,之前需填充3个字节,sum_b = sum_a + 3 = 16B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma
pack中较小者,前者为12后者为4,所以结构体对齐模数是4。sum_b是4的4倍,不需再次对齐。
综上3步,可知结构体的长度是16B,各数据成员在内存中的分布如图1所示。
例子2:
#pragma pack(2)
struct my_struct
{
char a;
long double b;
};
#pragma pack()
例子1和例子2不同之处在于例子2中使用了#pragma pack(2)编译参数,它强制指定对齐模数是2。
此例子Windows和Linux计算方法一样,如下:
步骤1:所有数据成员自身长度和:1B + 12B = 13B,sum_a = 13B
步骤2:数据成员a放在相对偏移0处,之前不需要填充字节;数据成员b为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是2,之前需填充1个字节,sum_b = sum_a + 1 = 14B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma
pack中较小者,前者为12后者为2,所以结构体对齐模数是2。sum_b是2的7倍,不需再次对齐。
综上3步,可知结构体的长度是14B,各数据成员在内存中的分布如图2所示。
例子3:
struct my_struct
{
char a;
double b;
char c;
};
前两例中,数据成员在Linux和Windows下都相同,例3中double的对齐模数在Linux中是4,在Windows下是8,针对这种模数不相同的情况加以分析。
在Windows中计算步骤如下:
步骤1:所有数据成员自身长度和:1B + 8B + 1B = 10B,sum_a = 10B
步骤2:数据成员a放在相对偏移0处,之前不需要填充字节;数据成员b为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是8,之前需填充7个字节,sum_b = sum_a + 7 = 17B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma pack中较小者,前者为8后者为8,所以结构体对齐模数是8。sum_b应该是8的整数倍,所以要在结构体后填充8*3 - 17 = 7个字节。
综上3步,可知结构体的长度是24B,各数据成员在内存中的分布如图3所示。
在Linux中计算步骤如下:
步骤1:所有数据成员自身长度和:1B + 8B + 1B = 10B,sum_a = 10B
步骤2:数据成员a放在相对偏移0处,之前不需要填充字节;数据成员b为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是4,之前需填充3个字节,sum_b = sum_a + 3 = 13B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma
pack中较小者,前者为8后者为4,所以结构体对齐模数是4。sum_b应该是4的整数倍,所以要在结构体后填充4*4 - 13 = 3个字节。
综上3步,可知结构体的长度是16B,各数据成员在内存中的分布如图4所示。
例子4:
struct my_struct
{
char a[11];
int b;
char c;
};
此例子Windows和Linux计算方法一样,如下:
步骤1:所有数据成员自身长度和:11B + 4B + 1B = 16B,sum_a = 16B
步骤2:数据成员a放在相对偏移0处,之前不需要填充字节;数据成员b为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是4,之前需填充3个字节,sum_b = sum_a + 3 = 19B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma pack中较小者,前者为4后者为4,所以结构体对齐模数是4。sum_b是4的整数倍,需在结构体后填充4*5 - 19 = 1个字节。
综上3步,可知结构体的长度是20B,各数据成员在内存中的分布如图5所示。
例子5:
struct my_test
{
int my_test_a;
char my_test_b;
};
struct my_struct
{
struct my_test a;
double my_struct_a;
int my_struct_b;
char my_struct_c;
};
例子5和前几个例子均不同,在此例子中我们要计算struct my_struct的大小,而my_struct中嵌套了一个my_test结构体。这种结构体应该如何计算呢?原则是将my_test在my_struct中先展开,然后再计算,即是展开成如下结构体:
struct my_struct
{
int my_test_a;
char my_test_b;
double my_struct_a;
int my_struct_b;
char my_struct_c;
};
此例子Windows中的计算方法如下:
步骤1:所有数据成员自身长度和:4B + 1B + 8B + 4B + 1B= 18B,sum_a = 18B
步骤2:数据成员my_struct_a为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是8,之前需填充3个字节,sum_b = sum_a + 3 = 21B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma pack中较小者,前者为8后者为8,所以结构体对齐模数是8。sum_b是8的整数倍,需在结构体后填充3*8 - 21 = 3个字节。
综上3步,可知结构体的长度是24B,各数据成员在内存中的分布如图6所示。

此例子Linux中的计算方法如下:
步骤1:所有数据成员自身长度和:4B + 1B + 8B + 4B + 1B= 18B,sum_a = 18B
步骤2:数据成员my_struct_a为了内存对齐,根据“结构体大小的计算方法和步骤”中第二条原则,其对齐模数是4,之前需填充3个字节,sum_b = sum_a + 3 = 21B
步骤3:按照定义,结构体对齐模数是结构体内部最大数据成员长度和pragma
pack中较小者,前者为4后者为4,所以结构体对齐模数是4。sum_b是4的整数倍,需在结构体后填充6*4 - 21 = 3个字节。

Post Reply