查询内网中关于内存对齐的资料发现,它们往往只谈论一个层面的问题,而不涉及或稍微涉及更高或更低层面的问题;而这对于喜欢抠根问底的同学来说,是比较难受的。这里对内存对齐相关的问题和答案做一个简要总结,较为复杂的解释这里不涉及,但我会给出相关文章链接。
一、问题简述
内存对齐问题总的来说,分为How 、Who和 Why,至于What这里不再赘述:
- Who:谁让数据在内存中对齐存放的?
- How:内存是如何对齐的,即内存对齐的表现形式?
- Why:为什么要内存对齐?该问题又可分成两个层面的问题:
- 为什么内存中的数据要以内存对齐的方式排布?
- 为什么处理器要以内存对齐的方式读取内存?
二、问题详述
1、谁让数据在内存中对齐存放的?
答案是:编译器或某类支持该操作的语言的程序员。在C/C++中,是可以精确控制数据在内存中的分布的,目的是使CPU能够更加高效的从内存中存取数据,但其实这往往不需要开发者自己来完成,因为默认的分布已经是被编译器优化过的,实际上执行了一个填充操作,具体解释见如下链接或者后文。
VS编译器举例:Alignment
2、内存如何对齐?
内存中存储的无非是指令和数据,那么,分析数据结构如何使用内存,可以有效帮助我们认识内存对齐的具体表现形式。网上有一大堆分析C/C++ struct存储结构的文章,主要涉及了这四个关键概念和一个隐含操作:
alignment :
A memory address a, is said to be n-byte aligned when a is a multiple of n bytes (where n is a power of 2). ——Data structure alignment
即一个地址是n字节的倍数,可称为n-字节
对齐,而n = 2k(k=0,1....m).
所以一个地址a,如果a%(2k)=0,那么a就是(2k)-字节
对齐。
-
natural alignment :
可翻译为自然对齐。如果数据的地址与其大小对齐,则称为自然对齐,否则称为未对齐。根据上文“对齐”的概念,对某变量value,如value.addr % value.size = 0,那么就可以说该变量自然对齐。- 基本数据类型,自然对齐值为该类型的size,如char的自然对齐值为1,int自然对齐值为4......不难理解的是,将这些变量起始位置放置于对齐边界上后(即
value.addr % value.size=0
处),编译器不用再对它进行任何额外的优化,猜测这应该是“自然对齐”名称的来源。 - 对于结构体,取值为结构体内成员的
natural alignment
,如果结构体里不断嵌套包含结构体,那么递归的计算natural alignment
,直到递归到基本数据类型,在反过来得到最外层结构体的自然对齐值。
- 基本数据类型,自然对齐值为该类型的size,如char的自然对齐值为1,int自然对齐值为4......不难理解的是,将这些变量起始位置放置于对齐边界上后(即
-
specified alignment :
由编译器或用户指定的对齐值(如#progma pack (x)
),只对结构体有作用。 -
effective alignment :
取natural alignment
与specified alignment
两者中的最小值。 -
隐含操作:具体来说,就要执行padding(填充)操作,所谓填充,就是在结构体成员中间或最后一个成员之后填充数据占位,填充的是什么可忽略。其目的是为了满足自然对齐的要求——不仅要满足结构体成员的自然对齐要求(中间填充),还要满足结构体本身自然对齐的要求(尾部填充)。
举例:
#pragma pack (8)
struct S1{
char a;
int b;
};
struct S2{
char c;
struct S1 d;
long long e;
char f;
};
int main()
{
struct S1 a;
struct S2 b;
printf("size of int, long long: %lu, %lu\n", sizeof(long),sizeof(long long));
printf("size of S1: %lu\n", sizeof(a));
printf("size of S2: %lu\n", sizeof(b));
return 0;
}
输出:
size of int, long long: 4, 8
size of S1: 8
size of S2: 32
分析:
**首先分析struct S1**:
自然对齐值为4,指定对齐值为8,得到结构体有效对齐值也为4.
char a——> 0x0000 % 1 = 0,自然对齐,占一个字节
int b——> 如取值0x0001,0x0002...有0x0001 % 4 != 0,0x0002 % 4 != 0......
直至取址0x0004。
因此0x0001~0x0003将被填充(这是中间填充)。
int b 占4个字节,因此最后一个字节地址为0x0007.
结构体成员存储完毕,但我们要保证整个结构体存储完毕后,
其下一个字节地址对于该结构体是按照有效对齐值对齐的,
因为内存中有可能是连续存储着一个结构体数组。
而它的下一个字节地址为0x0008,结构体有效对齐值为4,有0x0008 % 4 = 0,
满足对齐要求,因此不必进行尾部填充。结构体大小为8字节
adr offset element
------ -------
0x0000 char a;
0x0001 char pad0[3]; //填充3字节数据
0x0004 int b; //int b(0x0004-0x0007)
...
0x0007 int b;
------------------------------分割线-----------------------------------------------
**分析struct S2**
自然对齐值为8,指定对齐值为8,得有效对齐值为8
0x0000 char c; //1字节
0x0001 char _pad0[7]; 填充7字节数据(中间填充);
0x008 S1 d; //占8字节
0x0010 long long e; //占8字节
0x0018 char f;//1字节
0x0019 char _pad[7] //尾部填充7个字节
最后一个成员char f 的地址为0x0018,下一个地址为0x0019,
0x0019 % 8 != 0,因此需要尾部填充,填充7个字节,
因此该结构体在内存中最后的位置为0x001F,因此该结构体大小为 1+7(填充)+8+8+1+7(填充)=32字节。
3、为什么要内存对齐?
前文中讲过,Why的问题要分两个层面来问,首先是为什么编译器按照内存对齐的方式存储数据?其次是,处理器为什么按照内存对齐的方式读写内存中的数据?
实际上,之所以有第一个问题,是因为第二个问题的存在,也就是说,之所以按照内存对齐方式存储数据,是因为处理器是这么做的,而且只有这么做效率才会高。
数据的内存对齐存储
对于用内存对齐的方式存储数据,其详细解释见:
Data alignment: Straighten up and fly right
翻译后的版本:link
这篇文章总结的很好,不再多复述。只分析总结其中讲述的一个细节:
Double-byte memory access granularity Quad-byte memory access granularity
上图中,分别是双字节存取粒度和四字节存取粒度的处理器。而假设数据是非内存对齐方式存储的,位于[1,2,3,4]字节处。
双字节存取粒度:
当从内存中一次读取4个字节时,如果是从地址0处开始读,总共需要读2次,即第一次读[0,1],第二次读[2,3]。如果从地址1处开始读,则需要读3次,依次是[0,1],[2,3],[4,5],也就是说处理器一定是按照内存对齐的方式读取内存的,哪怕是想从地址1处开始取数据。
四字节存取粒度:
从地址0开始读,只需读一次[0,1,2,3];从地址1开始读,需要读两次[0,1,2,3] 和 [4,5,6,7]。
那么,是怎么取得最终的数据的呢?
How processors handle unaligned memory access上图很形象的描述了是如何取得最终的数据的。这里假设是MSB(大端字节序)。因为数据被存储在单元[1,2,3,4],因此按照上文所述,四字节处理器分别读取了[0,1,2,3]和[4,5,6,7],当就是把第一个值[0,1,2,3]读入到结果寄存器后,向左移动一个字节(去掉了0字节处对应的二进制数据),然后把第二个值[4,5,6,7]读入到临时寄存器,向右移动3个字节(去掉了5,6,7字节处对应的二进制数据),最后两者OR,最终结果存储于结果寄存器。
内存存取粒度:因为每次内存存取都会产生一个固定的开销,最小化内存存取次数将提升程序的性能。所以往往不是初学者认为的单字节,跟具体处理器有关,但不会出现3字节、5字节等奇数存取粒度的出现。
总的来说,内存对齐方式存储数据的目的有两点:
- 提高存取效率
- 因为有的处理器不支持非内存对齐方式存取,将影响可移植性。
处理器的内存对齐存取
该问题涉及处理器的架构设计、缓存的利用等知识,具体内容待之后添加。
网友评论