现在的位置: 首页 > 综合 > 正文

内存对齐

2018年02月12日 ⁄ 综合 ⁄ 共 6961字 ⁄ 字号 评论关闭

现在的一些处理器,需要你的数据的内存地址必须是对齐(align)的,即使不是必须,如果你对齐的话,运行的速度也会得到提升。虽然对齐会产生的额外内存空间,但相对于这个速度的提升来说,是值得的。

所谓对齐,就是地址必须能整除一个整数,这个就是对齐参数(alignment value)。合法的取值范围是1、2、4、6、16、……、8192。

怎样对齐呢?编译器帮你搞定。

怎样设置编译器的对齐方式呢?用#pragma pack( n )和__declspec(align(#))。
依据它俩,编译器是咋工作的?这个就是接下来要说的了。

#include <stdio.h>
#pragma pack( 1 )
struct A
{
char a;
short b;
char c;
};

int main()
{
printf("%d/n",sizeof(A));
return 0;
}

OK,下面对这个代码进行详细的分析。

用MSDN的话一言以蔽之:

“The alignment of a member (except the first one) will be on a boundary that is either a multiple of n or a multiple of the size of the member, whichever is smaller.”

翻译成中文,也就是:

“结构体中的数据成员,除了第一个是始终放在最开始的地方,其它数据成员的地址必须是它本身大小或对齐参数两者中较小的一个的倍数。”

P.S:注意上面所说的后面一句话,也就是说,结构体的数据成员的地址必须是本身大小和对齐参数中较小的那一个。

(1)在pack为1的时候,对齐参数是1,那么我们对这个结构体每一元素进行分析。

char a; // 第一个元素在[0]位置处

short b; //short两个字节,地址是min(1,sizeof(short))的倍数,即1的倍数[1~2]

char c; // 地址应该是min(1,sizeof(1))的倍数,从而即为[3]

故在pack为1的时候,输出的结果应该是4([0~3]),其中所有的元素都填满了。

(2)在pack为2的时候,同样按照上面的方法,我们继续来分析下。

Char a; //第一个占[0]位置。

Short b; //min(2,sizeof(short)),也就是必须为2的倍数,从而[2~3]

Char c;//min(2,sizeof(char)),也就是位1,地址为[4]

因此最后占据的大小是[0],[2~3],[4],整个结构体的大小size必须是2的倍数,所以应该是6(向上对齐至2的倍数)

(3)在pack为4的时候,同上,得到的结果是

[0],[2~3],[4],因此也是6.

然后我们对上面的这个结构体变换一下顺序,可以得到。

struct B

{

char a;

char b;

short c;

};

在#pragma pack(4)的情况下,输出却是4(注:上面的输出时6)

解释如下:

Char a;//占据一个字节,地址为【0】

Char b;//地址应该是min(4,sizeof(char)) = 1的倍数,也就是地址为【1】

Short c; //地址应该是min(4,sizeof(short)) = 2的倍数,也就是【2~3】

故总体占据的是【0~3】的连续单元,也就是4.

至此,我们对#prgama pack(n)的用法和对应的判定方法有了一个全新的认识。

特别提出:
sizeof(ao.a )还是1,sizeof(ao.b )还是2。

如果struct B中含有A的一个对象m_a,
struct B
{

A m_a;

}
则这个m_a对齐参数是A中最大的数据类型的大小(这里是short的2)和n中较小者。如果这个对齐参数是B中最大的话,最后B的大小也会与这个对齐参数有关。

m_a的对齐参数,由于是A的变量,所以采用A的对齐参数,也就是应该是A的最大元素个数和n中较小的值。而B的大小就要根据这个对齐参数来确定大小。

#include <iostream>
#include <stdlib.h>

#define NUM 1
using namespace std;

#pragma pack ( 16 )

typedef struct {
int a;
char b;
double c;
}test;

struct B
{
int a;
test b;
};
int main()
{
cout << "sizeof(int) = "<<sizeof(int) << endl;
cout << "sizeof(char) = " << sizeof(char) << endl;
cout << "sizeof(double) = " << sizeof(double) << endl;
cout << sizeof(test)<< endl;
cout << sizeof(B) << endl;
system("PAUSE");
return 0;
}

(1)在pack为1的时候,由于min中有一个为1,所以都是相邻存放的。

Sizeof(test)就是int+char+double的大小之和,即13.

而对应的sizeof(B)则是一个int和一个struct之和。Int占4B,而struct的对齐参数则是

Min(1,sizeof(max(A)),A中最大的元素师double类型的,也就是8,所以结果是min(1,8)=1,所以也是相邻存放的,而sizeof(A)的结果是13,所以直接是13+4 = 17.

此时,sizeof(B)的大小是17.

(2) 在pack为2的时候,此时min中有一个为2,对于test结构体,它的大小是4+2+8=14,因为在double的时候,min(2,8)=2,所以double类型的变量应该是2的倍数的地址,造成了char类型处空出了一个字节。总体就是14B。而对于B结构体而言,一个int占据4B,然后结构体的对齐参数采用min(2,max(A)),即min(2,8)= 2,由于是int,所以下一个地址是4,自然也是2的倍数,于是还是相邻存放。而A结构体的大小时14,于是B结构体的大小时14+4=18.

(3) 在pack为4的情况下。同样可以得到。此时对于A结构体的大小是4+4+8=16,因为double类型的必须是4的倍数,造成了char变量要占4个地方(实际只占一个,只是说这个地方空出来了3B),所以总体的大小为16.而同样对于B结构体,sizeof的结果是16+4 = 20,因为对于里面的成员要是min(4,8) = 4,而int恰好是4的倍数,所以相邻存放。于是就是16,20.

(4) 在pack为8的情况下(有所变化!!!),此时A结构体的大小是16,分析方法和上面相同,但是对于结构体B而言就有所区别,此时int还是4个字节,但是对于成员test结构体,它的对齐参数是min(8,max(A)) = min(8,sizeof(double) ) = 8也就是对齐参数是8,所以结构体变量要从地址为8开始,此时int就空出来了4B,从而最后求大小的时候应该是8+sizeof(A)= 8+16=24(最终测试结果如此)

(5)在pack为16的情况(以及以后的情况),结果是:A的大小为16B,而B的大小是24B。

总结:

(1) 对于一个由简单类型组成的结构体,它的大小是由每一个成员变量的地址决定的。我们要按照定义的顺序,分别求出来地址开始的地方。从地址为0开始,每一个变量都采取min(n,sizeof(x))//x表示该变量的类型;来确定起始地址是多少的倍数,然后开始计数,直到填满该数据。最后求出来总的大小。而且在pack>=2的时候最终的大小需要时2的倍数,有时候需要向上取大为2的倍数。而在pack为1的情况则不需要。

(2) 对于含有结构体成员的结构体,方法同上,只是在于对于结构体变量的对齐参数取法需要说明,具体就是min(n,结构体成员的最大元素的大小),就像上面的,结构体B中含有A成员,所以对齐参数就是min(n,sizeof(double))的大小,然后按照这个做法来取地址。

P.S:注意这里是pack而不是package,否则编译器会直接忽略#pragma package(),因为即使发生错误编译器也会直接忽略,而我们还是会默认认为编译器已经当做了字节按照n来处理。(某些博客上面的内容很容易让人误解或者晕倒!)

以上代码结果在Dev C++ , C-Free 5.0,VS 2010上均通过测试。

 

 

在上面讲到了关于pack的内存对齐和计算方法,这里继续讲实现内存对齐的另一种方式:__declspec( align(#) )

__declspec( align(#) )和#pragma pack( n )有密切联系。

当一个变量或结构体同时受两者影响时,前者的优先级高。

成员的地址决定于前者及后者,其要么是前者的倍数,要么是后者的倍数,要么是成员的大小的倍数,取最小。

结构体最后的大小于前者有关,其要么是前者的倍数,要么是结构体中最大偏移量的倍数,取最大。

要算出最后结果,必须知道两者的值或缺省值。

下面举一个例子来详细的分析:

#include <stdio.h>

#include "stdafx.h"
#include <stdlib.h>
//using namespace std;

#pragma pack( push, 4 )

__declspec( align(32) )struct D
{
int i1;
double d1;
int i2;
int i3;
};

int main()
{
cout << "sizeof(int) = "<<sizeof(int) << endl;
cout << "sizeof(char) = " << sizeof(char) << endl;
cout << "sizeof(double) = " << sizeof(double) << endl;
cout << sizeof(D) << endl;
system("PAUSE");
return 0;
}

这段代码在VS 2010中的运行结果是,sizeof(D)的大小为32,而在Dev C++,C-Free 5.0以及gcc中的结果都似乎20。下面我们来着重讲讲关于__declspec( align(#) )的用法:

正如前面所说的,当有__declspec( align(#) )和pack的时候,__declspec( align(#) )的优先级要高些。所以对于上面这个例子,我们首先来计算出来每一个的大小。

1. 成员的地址如何取?

规则:成员的地址要取pack(n),__declspec( align(m) ),以及成员自身大小这三者之间的最小值,也就是,min(n,m,sizeof(成员变量类型)),那么我们可以对每一个结构体的成员都进行分析。

第一个为int类型,占据4B,所以地址是[0~3].

第二个为double类型,它的地址要根据min(4,32,sizeof(double))来判断,所以应该是4的倍数,也就是相邻着int类型的i1存放。地址是[4~11]。

第三个为int类型,占据4B,同样应该是4的倍数,地址是[12~15].

第四个为int类型,占据4B,地址为[16~19].

从而总的地址是从[0~19]连续存放的20个字节,那么是否sizeof(D)的大小就是20呢?

经过测试,我们可以看到,在VS 2010中,结果是32,why?

这就要用__declspec( align(#) )来解释了。也就是下面第二点的内容。

2. 结构体最后的大小如何决定?

规则:结构体最后的大小与__declspec( align(m) )有关,其要么是它的倍数,要么是结构体中最大偏移量的倍数,取最大。

根据这个规则,这里align是32,而结构体中最大的是double类型,也就是应该是max(32,8)=32,所以最后结构体的大小应该是32的倍数,而明显上面我们看到的实际大小是20B,从而需要扩展到32B。

在这里,就体现了__declspec( align(m) )的强大作用!

同样的,为了体现该语句的作用,我们去掉这个语句,运用我们前面一节内容的知识,来计算并测试sizeof(D),最终不论是在VS 2010还是Dev C++中,运行的结果都是上面我们所预测的20B。

OK,下面回到最后的疑问,也就是前面我们提出的,为何加入了__declspec( align(m) )语句之后,在DevC++和VS 2010的结果不同?

实际上,对于这些内存对齐的处理,不同的编译器可能采取不同的处理,就像前面一节中所说的,我将pack误用为package,导致根本没有达到按照我要求的字节对齐的目的,而且编译器根本不提供任何警告信息。那么,这里合理的解释是:Dev C++不支持这种用法。

通过查阅资料,参照这篇文章【 SSE指令介绍及其C、C++应用 】(http://blog.csdn.net/delphihero/archive/2006/09/24/1270069.aspx),我们可以看到作者有这么一段话:

“接下来我举一个例子来说明SSE的指令函数是如何使用的,必须要说明的是我以下的代码都是在VC7.1的平台上写的,不保证对其它如Dev-C++、Borland C++等开发平台的完全兼容。”

“这里要注意一下,我使用了__declspec(align(16))做为数组定义的修释符,这表示该数组是以16字节为边界对齐的,因为SSE指令只能支持这种格式的内存数据。

我们在这里看到了SSE算的强大,相信它会成为多媒体程序员手中用来对付无穷尽流媒体数据的一把利剑。我后面还会写一些关于SSE算法更复杂应用的文章,敬请关注,感谢您抽时间阅读!

从这篇文章我们可以看到,SSE指令集的情况下,在VC 7.1下才支持__declspec(align(16))这种用法,而对于其他平台不一定有效。而前面我们使用的Dev C++以及C-Free,都是基于g++或者MinGW,不一定会支持这种方式,或者说,不一定按照这种内存对齐的建议来做,也就造成了结果的不同。

下面我们来继续探讨结构体中有结构体的情况。

先看看下面这段代码:

#include <stdio.h>

#include "stdafx.h"
#include <stdlib.h>
//using namespace std;

#pragma pack( push, 4 )

__declspec( align(32) )struct D
{
int i1;
double d1;
int i2;
int i3;
};

__declspec( align(16) ) struct E
{
int i1;
D m_d;
int i2;
};

int main()
{
cout << "sizeof(int) = "<<sizeof(int) << endl;
cout << "sizeof(char) = " << sizeof(char) << endl;
cout << "sizeof(double) = " << sizeof(double) << endl;
cout << sizeof(D) << endl;
cout << sizeof(E) << endl;
system("PAUSE");
return 0;
}

最后运行的结果是sizeof(E)为96,为何会是这个结果呢?我们来详细讲解下。

对于结构体E,第一个元素为int类型,所以占据[0~3]地址单元。

第二个元素是一个结构体,该结构体由于受上面__declspec( align(32) )的影响,优先级高,所以起始地址是32的倍数,而且大小为32B,从而应该放置在[32~63]单元处。

最后一个是int类型的变量,大小为4,所以应该是4的倍数,地址为[64~67]。

故结构体E的大小应该是从[0~67],占据68B,而由于前面还有限制__declspec( align(16) ),同时成员变量的最大偏移是sizeof(D)=32,所以我们最后这个结构体的大小应该是他们中最大值的倍数,也就是32的倍数,68向上取32的倍数应该是96.故结果为96.

最后仍然是上面平台的问题,在Dev C++和G++下面的结果不同,原因上面解释了。

MSDN:

“The sizeof value for any structure is the offset of the final member, plus that member's size, rounded up to the nearest multiple of the largest member alignment value or the whole structure alignment value, whichever is greater.”

中文:

“sizeof的结果都是结构体中最后的一个成员变量加上它的大小,再加上一个填充容量(padding),这个填充大小是成员变量最大的一个对齐参数或整个结构体的对齐参数的倍数,取哪个决定于哪个对齐参数较大”

ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_vclang/html/e4209cbb-5437-4b53-b3fe-ac264501d404.htm

ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_vclang/html/9cb63f58-658b-4425-ac47-af8eabfc5878.htm

P.S.:上面是关于内存对齐的研究,如有谬误,欢迎指出!

抱歉!评论已关闭.