C和C++内存对齐

2015/06/18 C和C++基础

对于所有直接操作内衬的程序员来说,数据对齐是很重要的问题,数据对齐与否能对程序的正常运行造成影响。

内存存取粒度

程序员通常倾向认为内存就像一个字节数组,顺序、均匀地在内存空间线性地排列开来。如图所示:

理论上CPU处理器可以按一个字节为单位来存取内存,但它一般不着么做。假设CPU是一个32位的处理器,它一般会以双字节或者最多以四字节为单位来存取内存,我们把这些存取内存单位称为内存存取粒度

至于计算机为什么存取内存一般采用一个字节以上的内存存取粒度,答案很简单:比如有一个4字节的变量,使用1个字节的内存存取粒度模式从内存读到CPU寄存器中需要四次读取操作

但是如果使用2个字节的内存存取粒度,仅需要两次读取操作,读取内存效率就会提高一倍。

内存存取粒度规定每一次访问数据的大小,同时对地址上也有限制。每一次访问的空间的首地址必须是内存存取粒度的倍数。比如内存存取粒度为4,只能访问这样的区间:

0x0 - 0x3, 0x4 - 0x7, 0x8 - 0xc

不能访问这样的区间:

0x1 - 0x4, 0x5 - 0x8, 0x6 - 0x9

因此在CPU处理器看来,内存是块为单位的:

字节对齐

假设机器子长是32位(4个字节),也就是处理任何内存中的数据,其实都是按32位的单位进行的,内存存取粒度为4。现在有2个变量:

char A;
int B;

假设这2个变量是从内存0开始分配的,理论上应该是这样存储的:

因为计算机的内存存取粒度为4,所以在处理变量A与B时的过程大致为:

A:将0x00-0x03共32位读入寄存器,在通过右移24位(或者与0x000000FF做与运算)得到a的值

B:将0x00-0x03共32位读入寄存器,通过位运算符得到低24位的值;在将0x04-0x07这32位读入寄存器,通过位运算得到高8位的值;再与最先得到的24位做位运算,才可以得到整个32位的值

由此可知,对a的处理是最简单的。可对b处理,本身是一个32位的数,处理的时候却得折成2部分,之后再合并,效率上就有些低了。

要解决这个问题,就需要付出几个字节浪费的代价:

按照上面的分配方式:A和B之间填充了3个空字节,A的处理过程不变,B却变得简单多:只需将0x04-0x07这32位读入寄存器即可,读取效率大为提高。这就是字节对齐

字节对齐概念:因为计算机的内存空间都是按照字节来划分的,从理论上讲任何类型的变量访问可以从任何地址开始。但实际情况是在访问特定类型变量的时候,经常在特定的内存地址访问,这就需要各种类型数据要按照一定的规则在内存空间上排列,而不是顺序的一个接一个地排放

字节对齐是为了在内存空间与复杂度上达到平衡的一种技术手段,简单地来说,是为了在可接受的空间浪费的前提下,尽可能地提高内存存取效率(相同运算过程的最快处理)

如图所示,内存存取粒度为4字节,左边表示把内存0x00-0x03的数据读到寄存器中,由于满足字节对齐状况,只需要一次读出操作;右边表示把内存0x01-0x04的数据读到寄存器中需要两次读取操作,还有额外的数据位运算,内存存储效率降低:

非对齐数据处理过程(把内存0x01-0x04的数据读到寄存器中):

处理器先从非对齐地址读取第一个4字节块,剔除不想要的字节,然后读取下一个4字节块,同样剔除不要的数据,最后留下的两块数据合并放入寄存器,这需要做很多工作。

目前计算机一般是32位和64位,而64位是主流。因此内存存取粒度可以设置为1字节,2字节,4字节,最大为8字节。当把内存存取粒度设置为1字节,则不需要数据进行字节对齐,因为CPU在该模式下可以读取任何内存单元。当我们把内存存取粒度设置为2字节、4字节、8字节时,在内存中的数据也要按2字节对齐、4字节对齐、8字节对齐。在Window VS2010环境下,默认8字节对齐(内存存取粒度为8)

结构体数据成员字节对齐

字节对齐主要是为了解决内存访问次数的问题,确保访问一个变量以最少的读取次数来完成访问过程

例子1

假设内存存取粒度为4,有如下变量:

char a;
int b;

假设如图存放数据:

访问变量a,只需读取一次,访问变量b则需要读取两次(先读取0x00-0x03,再读取0x04-0x07,然后运算得到结果。

如果按如下图存放数据:

访问变量a,只需读取一次,访问变量b也只读取一次(只读取0x04-0x07)。

这就说明如果一个变量,如果跨越了4字节边界存储,那么cpu要读取两次,这样效率就会降低。在图中0x03-0x04就是一个4字节边界。因此必须尽可能避免跨越边界存储数据

例子2

假设内存存取粒度还是为4,有如下变量:

char a;
double b;

假设如图存放数据:

访问变量a只需读取一次,访问变量b需要读取三次(先读取0x00-0x03,再读取0x04-0x07,最后读取0x08-0x0B),跨越4字节边界0x03-0x04和0x07-0x08存储两次

如果按如下图存放数据:

访问变量a只需读取一次,访问变量b需要读取两次(读取0x04-0x07,再读取0x08-0x0B),读取次数变得最少(因为内存存取粒度为4,而double类型变量大小为8,因此至少需要两次读取),只跨越4字节边界0x07-0x08存储一次

例子3

假设内存存取粒度为8,有如下变量:

char a;
double b;

必须按如下图存放数据:

才能做到访问变量a只需读取一次,访问变量b只需要读取1次(读取0x04-0x0B),读取次数最少(因为内存存取粒度为8,而double类型变量大小为8,因此至少只需要一次读取),没有跨越8字节边界存储

例子4

假设内存存取粒度为8,有如下变量:

char a;
int b;

对于这种情况,变量b可以4字节对齐:

也可以8字节对齐:

无论是哪一种对齐方式,访问变量b都只需要读取一次,没有跨越8字节边界存储,但是明显4字节对齐比8字节对齐更有优势,因为4字节对齐浪费了3个字节的空间,而8字节却浪费了7个字节,因此,综合考虑内存空间浪费问题,我们选择MIN(内存存取粒度8,该数据成员的自身长度4) = 4来对齐。

总结

综上以上三个例子,对于一个结构体变量来说,假设结构体第一个数据成员从内存地址0x00开始:

字节对齐规则1:每个数据成员的偏移量必须是MIN(内存存取粒度n,该数据成员的自身长度)的倍数

这条规则保证了跨越n字节边界存储的次数最少,读取操作也就最少,提高了内存读取效率。但是前提是结构体第一个数据成员的内存地址是0x00,现实情况是不可能的,因此引出第二条规则:

字节对齐规则2:结构体第一个数据成员的地址必须是MIN(内存存取粒度n,结构体中数据成员最大长度)的倍数

至于为什么是结构体中数据成员最大长度,请看以下例子:

例子5
struct{
	int a;
	short b;
}

在这里int是4字节,short是2字节,假设结构体开始地址以2字节为倍数,如图所示:

则b变量可以满足字节对齐规则1,但变量a无法满足字节对齐规则1。

假设结构体开始地址以4字节为倍数,如图所示:

则变量a和b满足字节对齐规则1,

因此字节对齐规则2保证结构体后面每个数据成员的地址MIN(内存存取粒度n,该数据成员的自身长度)的倍数。

例子6

以下代码在VS2010环境运行,内存存取粒度默认为8字节(默认8字节对齐):

struct T
{ 
    char c;     //本身长度1字节 
    __int64 d;  //本身长度8字节
    int e;      //本身长度4字节
    short f;    //本身长度2字节
    char g;     //本身长度1字节
    short h;    //本身长度2字节
}stu;

分析:

代码中定义了结构体变量stu,假设在内存中分配到了00(0x00)的位置。

对于成员stu.c,只需一次寄存器读入,所以先占一个字节;

对于成员stu.d,是一个8字节的变量,如果紧跟着stu.c存储,则读入寄存器需要两次,因此按照8字节对齐存储,分配到08-15(0x08-0x0F),只需一次寄存器读入;

对于变量stu.e,是一个4字节的变量,4字节对齐即可,分配到16-19(0x10-0x13)单元中,只需一次寄存器读入;

对于成员stu.f,是一个2字节的变量,2字节对齐即可,分配到20-21(0x14-0x15)单元中,只需一次寄存器读入;

对于成员stu.g,是一个1字节的变量,存储在任何字节都是对齐的,所以分配到22(0x16)的单元中,只需一次寄存器读入;

对于成员stu.h,是一个2字节的变量,2字节对齐即可,分配到24-25(0x18-0x19)单元中,只需一次寄存器读入;

则得到如下分配图:

到这里还没有结束,如果定义一个结构体数组 stuA[2],按变量分配的原则,这2个结构体应该是在内存中连续存储的,分配应该如下图:

stuA[0]数据保证对齐了,但是stuA[1]的很多成员都不再对齐了,究其原因,是违反了字节对齐规则2:即stuA[1]结构体第一个数据成员的地址不是MIN(内存存取粒度n,该数据成员的自身长度)的倍数,stuA[1]结构体的开始边界不对齐。解决办法是:

字节对齐规则3:结构体长度一定是MIN(内存存取粒度n,结构体中数据成员最大长度)的整数倍

如果stuA[0]的长度是MIN(内存存取粒度n,该数据成员的自身长度)的整数倍,那么stuA[1]结构体第一个数据成员的地址一定是MIN(内存存取粒度n,该数据成员的自身长度)的倍数,从而满足字节对齐规则2,stuA[1]的数据成员也就都对齐了,如图所示:

因此结构体变量分配到25字节单元中,25不是8的整数倍,因此取最近的整数32作为结构体变量的大小,从26字节开始,后面的字节单元全部被浪费掉。所有这些内存字节对齐工作都由编译器来完成

指令#pragma pack(n)

#pragma pack(n)用来设置内存存取粒度为n,C编译器将按照n个字节对齐。

例子7
#include <iostream>
#include <stdio.h>

using namespace std;

#pragma pack(4)   //设置4字节对齐

struct T
{ 
    char c;     //本身长度1字节 
    __int64 d;  //本身长度8字节
    int e;      //本身长度4字节
    short f;    //本身长度2字节
    char g;     //本身长度1字节
    short h;    //本身长度2字节
}TT;

int main(){
	//按照上面所说的方法推算 输出应是24个字节
	cout<<"sizeof(TT)="<<sizeof(TT)<<endl;
	system("pause");
}

#####例子8

#include <iostream>
#include <stdio.h>

using namespace std;

struct stu1{
	char sex;
	int length;
	char name[10];
}my_stu1;

struct stu2{
	int length;
	char name[10];
	char sex;
}my_stu2;

struct stu3{
	short length;
	char sex;
}my_stu3;

struct stu4{
	int length;
	double sex;
}my_stu4;

struct stu5{
	char x1;
	short x2;
	float x3;
	char x4;
}my_stu5;

int main(){

	cout<<"sizeof(my_stu1)="<<sizeof(my_stu1)<<endl;            //20
	cout<<"sizeof(my_stu2)="<<sizeof(my_stu2)<<endl;            //16
	cout<<"sizeof(my_stu3)="<<sizeof(my_stu3)<<endl;            //4
	cout<<"sizeof(my_stu4)="<<sizeof(my_stu4)<<endl;            //16
	cout<<"sizeof(my_stu5)="<<sizeof(my_stu5)<<endl;            //12

	system("pause");
}
例子9
#include <iostream>
#include <stdio.h>

using namespace std;

#pragma pack(4)

struct stu1{
	char sex;
	int length;
	char name[10];
}my_stu1;

struct stu2{
	int length;
	char name[10];
	char sex;
}my_stu2;

struct stu3{
	short length;
	char sex;
}my_stu3;

struct stu4{
	int length;
	double sex;
}my_stu4;

struct stu5{
	char x1;
	short x2;
	float x3;
	char x4;
}my_stu5;

int main(){

	cout<<"sizeof(my_stu1)="<<sizeof(my_stu1)<<endl;            //20
	cout<<"sizeof(my_stu2)="<<sizeof(my_stu2)<<endl;            //16
	cout<<"sizeof(my_stu3)="<<sizeof(my_stu3)<<endl;            //4
	cout<<"sizeof(my_stu4)="<<sizeof(my_stu4)<<endl;            //12
	cout<<"sizeof(my_stu5)="<<sizeof(my_stu5)<<endl;            //12

	system("pause");
}
例子10
typedef struct A 
{ 
    char c;      //1个字节
    int d;       //4个字节,要与4字节对齐,所以分配至第4个字节处
    short e;     //2个字节, 上述两个成员过后,本身就是与2对齐的,所以之前无填充
 };              //整个结构体,最长的成员为4个字节,需要总长度与4字节对齐,所以, sizeof(A)==12 

typedef struct B 
{ 
    char c;      //1个字节
    __int64 d;   //8个字节,位置要与8字节对齐,所以分配到第8个字节处
    int e;       //4个字节,成员d结束于15字节,紧跟的16字节对齐于4字节,所以分配到16-19
    short f;     //2个字节,成员e结束于19字节,紧跟的20字节对齐于2字节,所以分配到20-21
    A g;         //结构体长为12字节,最长成员为4字节,需按4字节对齐,所以前面跳过2个字节, 到24-35字节处
    char h;      //1个字节,分配到36字节处
    int i;       //4个字节,要对齐4字节,跳过3字节,分配到40-43 字节
};               //整个结构体的最大分配成员为8字节,所以结构体后面加5字节填充,被到48字节。故:sizeof(B)==48;

Search

    Post Directory