从底层来说,计算机访问内存的两种方式:
从为word_size(比如32位或者64位)的倍数的地址开始,
1. 读取长度为word_size的内存块的数据,或者
2. 把长度为word_size的数据写入内存。
这里有两个“天性”【注释1】:
1. 读/写总是从word_size的倍数的地址开始的(wikipedia上Data Structure Alignment词条独独少了这条至关重要的说明,虽然从其上下文可以推断出来)
2. 读/写的长度总是word_size的倍数
比如在一台64位的计算机上,读取一块1k的内存,CPU需要进行的读取操作次数是:
基于这两个天性,内存对齐这个概念,包含两个方面:
1. 内存块地址的对齐
指的是内存块的地址(即一块内存的首字节的地址)是word_size的倍数;用几个例子来说明这个意思:
1)在一台32位计算机上的内存里,某个int型变量i在内存中的地址是0x7B163E00;
2)某个char变量c在内存中的地址是0x7B163D00,等等
内存地址不对齐,会引起什么样的问题呢?用一个例子来说明:
假定在一台32位机器上,有一个整型变量i的地址是34 【注释2】,那i存储在内存的34、35、36、37地址;
为了把这个变量从内存读进CPU,由于计算机从内存读取数据的天性(第一点,读/写总是从word_size的倍数的地址开始的),需要两次读取(第一次从32开始读32 33 34 35,第二次从36开始读36 37 38 39),然后把第一次读取的后两个字节(34 35)抽取出来,把第二次读取的前面两个字节(36 37)抽取出来拼到一起组成变量i:
� 一个int变量为4bytes,即32位,从CPU一次可以读取的内存块长度来看,本可以一次读完;但是因为这个变量的内存块地址没有对齐,将导致本来一个read指令就能完成的读取操作,需要两次read外加其它复杂的抽取拼接计算,从而大大地降低了性能。
2. 内存长度的对齐
指的是某个内存块里,存的各个数据占的长度总是word_size或者是其倍数;从实际应用来说,这一点不总是�满足的。比如说有这么一个结构体:
�struct ExampleStruct {
char b;
int a;
};
结构体ExampleStruct包含两个数据成员:b(char型)和a(int型);其中a占的内存恰好是word_size,但b占的却不足word_size【注释3】;不失一般性,假定char型变量占两个字节;这个时候,为了使内存对齐,一般会在这个char变量后面“加塞”�两个字节无意义的数据,使得这个char“占用”的内存和int一样长,达到word_size,即4字节。
如果不进行这种“加塞”,会引起什么问题呢?同样,用一个例子来说明:
假定现在有一个ExampleStruct的数组a[2],里面有两个ExampleStruct的对象a[0], a[1];由于数组的内存在分配的时候相邻的元素总是在相邻的内存地址上的,于是a[0]占的内存长度是4 + 2字节等于6字节,假定它从32地址开始,则其在内存中占用的空间为32 33 34 35 36 37;因为a[0]和a[1]连续存储,于是a[1]占用的内存是38 39 40 41 42 43;从而a[1]的首字节地址为38,不是word_size(即4)的倍数,于是引发1中说到的内存块地址未对齐时引发的问题。
注释:
注释1:天性:与生俱来的特性;这里所说的计算机天性,指的内存访问(即读或写)的方式,是在计算机生产制造的时候就确定并现实了的。
注释2:此处为简单起见,不失一般性,使用34这种“人类可读”的地址,而不再使用0x7B163E00等这种“原生”地址
注释3:一般来说,现代�编译器在实现的时候,一个int(整型)的长度,等于目标机器的字长;而char(字符型)一般比int(整型)小。