内存对齐

1.引入

    现在我们有一个结构体student,当我们以不同的顺序排列结构体中的字段时会出现占用字节大小不相同的情况,如下:

type student struct {
	id   int16
	name string
	boy  bool
}

fmt.Println(unsafe.Sizeof(student{})) // 32
-----------------------------------------------------------------------
type student struct {
	name string
	id   int16
	boy  bool
}

fmt.Println(unsafe.Sizeof(student{})) // 24

    我们使用的机器是win64位,之所以会出现这样的现象,是因为编译器自动给我们做了内存对齐,下边我们来逐步认识一下什么是内存对齐,以及它有什么优势.

2.内存对齐概念

       现代计算机中内存空间都是按照字节(byte)进行划分的,所以从理论上讲对于任何类型的变量访问都可以从任意地址开始,但是在实际情况中,在访问特定类型变量的时候经常在特定的内存地址访问,所以这就需要把各种类型数据按照一定的规则在空间上排列,而不是按照顺序一个接一个的排放,这种就称为内存对齐,内存对齐是指首地址对齐,而不是说每个变量大小对齐。

  • 对齐系数

    编译器默认对齐系数:

      32位系统: 对齐系数为4

      64位系统: 对齐系数为8

      不同的硬件系统对齐系数可能不相同,在C语言中甚至可以通过预编译指令修改对齐系数.

    常见数据类型大小及对齐系数(64位):

数据类型 大小 对齐系数
bool 1 1
byte 1 1
int8 1 1
int16 2 2
int32 4 4
int64 8 8
float32 4 4
float64 8 8
string 16 8
  • 偏移量

    以结构体为例,结构体在内存中占有一块内存,结构体中的字段存储在这块内存中,偏移量可以简单理解为每个字段存储的地址到这个结构体首地址的差值.

           

  • 内存对齐规则

    规则一:结构体第一个成员变量偏移量为0,后面的成员变量的偏移量等于成员变量大小和成员对齐系数两者中较小的那个值的最小整数倍,如果不满足规则,编译器会在前面填充值为0的字节空间;

        成员偏移量 = MIN(成员变量大小, 成员对齐系数) * n

    规则二:结构体本身也需要内存对齐,其大小等于各成员变量占用内存最大的和编译器默认对齐系数两者中较小的那个值的最小整数倍.

        结构体偏移量 = MIN(MAX(成员1,成员2,......), 编译器默认对齐系数) * n

3.为什么要内存对齐

  a. 提高内存的访问速度

      CPU总是以其字大小(64位处理器上为8个字节)读取数据, 所以当您在支持它的处理器上进行未对齐的地址访问时, 处理器将读取多个字. CPU将读取您请求的地址所跨越的每个内存字. 这将导致访问请求数据所需的内存事务数增加至多2倍.

      

      现在假设我们有a,b两个变量, 其中a占2个字节, b占4个字节, cpu的访问粒度为4(即每次读取4字节), 现在我们想读取b的值.

      内存未对齐时:

        cpu第一次读取获得(0~3)的地址,发现其中0,1不属于b变量,舍弃0,1,但已知b是一个4字节的变量,所以要继续读取;cpu第二次读取获得(4~7)的地址,其中6,7不属于b变量,舍弃6,7, 最后将2,3,4,5按照特定算法结合到一起,此时即获得变量b的值.发现要想获取到变量b,不仅要读取多次,而且还需要舍弃和结合的操作,这无形中占用了很多时间;

      内存对齐时:

        cpu发现第一个地址4在对齐边界上(根据对齐规则一,a现在占用4字节,2,3位置被填充并划给变量a), 直接从4开始读取,获取到(4,5,6,7), 只需读取一次就能获取到变量b, 典型的用空间换时间.

  b.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的; 某些硬件平台只能在某些地址处取某些特定类型的数据, 否则抛出硬件异常。

4.内存对齐由谁来完成

  内存对齐通常是由硬件系统和编译器来完成的.

5.go语言中的内存对齐

  在go语言中unsafe包可以查看变量所占的空间大小,变量成员相对于变量在内存中的偏移量,变量成员的对齐系数. 回到引入中提到的问题,为什么因为结构体成员顺序的不同就会造成结果体所占用内存空间大小的不同呢?下边我们做个简单分析:

type student struct {
	name string
	id   int16
	boy  bool
}
fmt.Println("占用空间: ", unsafe.Sizeof(student{})) fmt.Println("name大小: ", unsafe.Sizeof(student{}.name), ", name偏移量: ", unsafe.Offsetof(student{}.name), ", name对齐系数: ", unsafe.Alignof(student{}.name)) fmt.Println("id大小: ", unsafe.Sizeof(student{}.id), ", id偏移量: ", unsafe.Offsetof(student{}.id), ", id对齐系数: ", unsafe.Alignof(student{}.id)) fmt.Println("boy大小: ", unsafe.Sizeof(student{}.boy), ", boy偏移量: ", unsafe.Offsetof(student{}.boy), ", age对齐系数: ", unsafe.Alignof(student{}.boy)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~结果:~~~~~~~~~~~~~~~~~~~~~~~~~~ 占用空间: 24 name大小: 16 , name偏移量: 0 , name对齐系数: 8 id大小: 2 , id偏移量: 16 , id对齐系数: 2 boy大小: 1 , boy偏移量: 18 , age对齐系数: 1 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

  当我们把成员中字节占用比较大的name放在第一位时,结构体占用了24字节.我们依次分析:

  根据规则一:

    name: string类型,类型大小为16字节,对齐系数为8,因为是第一个成员,所以偏移量为0;

    id: int16类型,类型大小为2, 对齐系数为2,Min(成员大小, 对齐系数) = 2, 因为name占用了16字节, 所以必须2*n>=16才行,取n的最小值,所以n=8, 偏移量 = 2 * 8, 所以id占用的内存地址为第17和18;

    boy: bool类型,类型大小为1, 对齐系数为1, Min(成员大小,对齐系数) = 1, 因为name和id已经占用了18字节, 所以必须2*n>=18, 取最小,n=9, 偏移量 = 2 * 9, 所以boy占用内存地址为第19.

    此时的内存占用为: 16 + 2 + 1 = 19字节.

  根据规则二:

    我用的是64位系统,所以编译器对齐系数为8, 则MIN(MAX(成员1,成员2,......), 编译器默认对齐系数)便成为Min(Max(16,2,1), 8) = 8, 因为上边已经占用了19字节,所以8*n >= 19, 很明显最小的n为3,所以总内存为8*3=24字节,正好与代码计算的值相同.

  接下来我们对结构体成员的顺序做调整:

type student struct {
	id   int16
	name string
	boy  bool
}

  这次我们调整了id和name的顺序, 再次进行分析:

  根据规则一:

    id: int16类型,类型大小为2,对齐系数为2,因为是第一个成员,偏移量为0;

    name: string类型,类型大小16,对齐系数为8, Min(16, 8) = 8, id已经占用2字节, 所以8*n>=2, n最小取1, 偏移量 = 8 *1, 那么name在内存中就要从第9个地址开始,到第25地址, 那么有人问id值占了2字节,那3~8地址不就空着吗?是的,就是这样,系统会给这些地址做0填充,典型的用空间换时间的做法;

    boy: bool类型,类型大小为1,对齐系数为1,Min(1,1) = 1,id和name占用了8 +16 = 24字节,所以1*n>=24, 取n最小值,n=24,偏移量 = 1 * 24, boy存储在第25地址上.

    此时内存占用为: 8 + 16 +1 = 25字节.

  根据规则二:

    Min(Max(2,16,1), 8) = 8, 上边已经占用了25字节,  所以8*n >= 25, n最小为4, 所以总内存占用为32字节.这就很好的解释了引入中存在的问题.  

6.总结

  内存对齐就是定制了一套规则, 以合理的利用内存空间并提高内存访问效率. 编译器通过适当增加padding, 使每个成员的访问都在一个指令里完成, 而不需要多次访问再拼接. 是一个以空间换时间的过程.

 

posted @ 2022-05-11 14:24  屁桃  阅读(231)  评论(0编辑  收藏  举报