总结

  • 内存对齐的两要素:数据起始地址要等于内存边界的倍数、数据所占字节数要是对齐边界的倍数
  • 32位系统中最大对齐边界是4个字节,64位系统是8个字节、
  • 这里的最大对齐边界,和机器每次能操作的字节数相等,也就是机器字长
  • go语言中,取类型大小和最大对齐边界的最小值,作为类型的对齐边界
  • 为什么要内存对齐,为了提高性能,降低内存的浪费

内存

cpu在内存中读取数据的步骤

  1. 将内存地址通过地址总线传输给内存
  2. 内存根据地址准备好数据
  3. 内存将数据通过数据总线传输给cpu

image.png
如果地址总线只有八根,那这个地址只有八位,可以表示256个地址,所以256byte就是八根地址总线最大的寻址空间,要想使用更大的内存,就需要更大的地址总线,32位地址总线(0,2^32-1),可以选址4G内存
需要操作4字节,就最少需要32位数据总线,8字节就需要64位,这里每次操作的字节数就是所谓的机器字长
**

内存介绍

如果内存就像我们逻辑上认为的那样,一个挨一个行成一个大矩阵,我们可以访问任意地址,并把它输出到总线
image.png
但是为了更高的访问效率,最典型的内存布局如下

一个内存条的一面是一个rank,包含了多个chip,一个chip包含了8个bank,到了bank就可以通过选择行选择列来定位一个地址了

image.png
这样的结构不是我们逻辑上认为的那样,但是8个bank公用同一个地址,获取数据时,各自选择同一个位置的一个字节,组合起来作为我们逻辑上认为的连续的八个字节,通过这样的并行操作提高了内存的访问效率
image.png
但是使用这样的设计,那么address这里的地址就只能是8的倍数,如果非要错开一个字节,那么最后最后一个地址的位置和前面七个不同,不能再一次操作中被同一个地址选中,所以这样的地址是不能用的(硬件不支持),如下图
image.png
有一些cpu支持访问任意地址,是因为它多做了很多处理,如下:
如果我们取8字节的数据,从地址0开始去,那么cpu将会分两次获取,第一次获取前八个字节,只留后7个字节,第二次取后八个字节,只留第一个字节,组成我们需要的数据
image.png

内存对齐

概念

上面那样操作必然会影响到性能,所以为了保证程序高效的运行,编译器会把各种类型的数据安排的合适的地址,并占用合适的长度,这就是内存对齐

对齐边界

每种类型的对齐值就是他的对齐边界,内存对象要求数据的地址和占用字节数都要死它对齐边界的倍数,所以下面的int32要从4开始,却不能紧接着从2开始
image.png

如何确定对齐边界

对齐边界和平台也有关系,go语言支持下面的平台,在32位平台上,指针宽度寄存器宽度都是4字节64位机器上都是8字节,而被go语言成为寄存器宽度的这个值,就是机器字长,也是平台对应的最大对齐边界
image.png
数据类型的对齐边界,取类型大小和平台最大内存边界的较小的那个,需要注意相同的类型不同平台的类型大小不同,所以对齐边界可能也不同

内存对齐边界这样设计依然是为了降低内存浪费,提升性能
image.png

确定结构体的对齐边界

首先要确定每个成员的对齐边界,然后其中最大的作为结构体的对齐边界

例:
试试存储下面这个结构体

  1. type T struct {
  2. a int8
  3. b int64
  4. c int32
  5. d int16
  6. }

首先内存对齐的第一个要点,起始地址是要是对齐边界的倍数
结构体存储时,每个成员都要将结构体的起始地址当做地址0,再根据相对地址确定自己该放在什么位置
数据全部存好后并没有结束,要判断结构体占用的字节数是否是对齐边界的倍数,如果不是需要往后扩张一下,下图中结构体占用了22个字节,并不是8的倍数,所以需要往后扩张一下,所以扩张到相对地址23,最终这个结构体类型的大小就是24个字节
image.png

为什么要规定,类型的大小要等于对齐边界的整数倍?
为了保证内存中每一个数据都内存对齐。
如果不做扩张的话,那么上面那个结构体的类型大小就是22,假设这是一个长度为2的T类型的数组,那么这个数组会占用44字节的内存,第一个元素是0-21第二个元素是22-44,那么这个时候第二个元素并没有内存对齐,如下图:image.png
如何优化这个结构体,降低内存占用呢?

  1. type T struct {
  2. a int8
  3. d int16
  4. c int32
  5. b int64
  6. }

结构体字段的顺序换成这样,只会占用16个字节