golang - 内存对齐

1. 介绍

CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度)
为保证程序顺利高效的运行,编译器会把各种类型的数据安排到合适的地址,并占用合适的长度,这就是内存对齐

示例:
假设cpu的memory access granularity =8 byte, 内存不对齐的情况下
,cpu 要从抵制1开始读8字节的数据,如下图所示:


示例

此时cpu 需要读取两次,

  • 第一次读取0-7 地址段
  • 第二次读取8-15 地址段
  • 两个地址段拼接,读取1-8地址的数据

相反的,如果在内存对齐的情况下, 1- 8 地址段的数据,将会直接存储在8-15 地址段,cpu 就可以一次获取到对应的字段。 但是之前的1-7地址段的内存就会被浪费了

总结来说,内存对齐的原因有如下两点:

  1. 平台(移植性)原因:不是所有的硬件平台都能够访问任意地址上的任意数据。例如:特定的硬件平台只允许在特定地址获取特定类型的数据,否则会导致异常情况
  2. 性能原因:若访问未对齐的内存,将会导致 CPU 进行两次内存访问,并且要花费额外的时钟周期来处理对齐及运算。而本身就对齐的内存仅需要一次访问就可以完成读取动作

2. 对齐边界/ 对齐系数

在不同平台上的编译器都有自己默认的 对齐系数(对齐边界),一般来讲,我们常用的平台的系数如下:

32 位系统:4byte(可以理解为cpu的memory access granularity=4)
64 位系统:8byte (可以理解为cpu的memory access granularity=8)

在go 语言中,每个数据类型,也有自己对应的对齐系数,我们可以通过官方的unsafe 包中的unsafe.Alignof 函数获取不同数据类型的对齐边界

func main() {
   fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true)))
   fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0)))
   fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0)))
   fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0)))
   fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0)))
   fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY"))
}
  • 对于数组类型 []elementType,对齐系数 = unsafe.Alignof(elementType)
  • 对于slice , map,指针类型, 对齐系数 = 8(64位操作系统)
  • 对于结构体, 首先要确定每个成员的对齐边界,然后取其中最大的,这就是这个结构体的对齐边界


    结构体内存系数

注意:
所有类型的对齐系数不会超过编译器设定的对齐系数, 例如在64 位操作系统中的默认对齐系数是8 ,所有的类型对齐系数不会超过8

3. 对齐规则

  1. 存储这个结构体的起始地址,是对齐边界的倍数。假设从0开始存,结构体的每个成员在存储时,都要把这个起始地址当作地址0,然后再用相对地址来决定自己该放在哪里。
  1. 结构体整体占用字节数需要是类型对齐边界的倍数,不够的话要往后扩张一下
  • 示例:

结构体T1 的两个成员的bool 和i6的大小分别占1byte 和2byte, 对应的对齐系数分别是1byte 和2byte, 此时结构体T1的对齐系数是Max{1,2}=2byte

type T1 struct {
  bool bool  
  i16  int16 
}

func main() {
    fmt.Printf("bool size:%v, bool align:%v\n", unsafe.Sizeof(bool(true)), unsafe.Alignof(bool(true)))
    fmt.Printf("int16 size:%v, int16 align:%v\n", unsafe.Sizeof(int16(0)), unsafe.Alignof(int16(0)))
    fmt.Printf("T1 size:%v, T1 align:%v\n", unsafe.Sizeof(T1{}), unsafe.Alignof(T1{}))
    return
}

在内存未对齐的状态下 T1 的大小为3字节, 但是通过内存对齐后,内存占4 字节,如图1 所示:

i16并没有直接放在bool的后面,而是在bool中填充了一个空白后,放到了偏移量为2的位置上。如果i16从偏移量为1的位置开始占用2个字节,根据对齐原则1:构体变量中成员的偏移量必须是成员大小的整数倍,套用公式 1 % 2 = 1,就不满足对齐的要求,所以i16从偏移量为2的位置开始

图1

以 结构体T2,T3 为例

type T2 struct {
  i8  int8  
  i64 int64
  i32 int32 
}


type T3 struct {
  i8  int8  
  i32 int32 
  i64 int64 
}

i8,i32,i64 的大小分别是1字节,4字节,8字节,对齐系数1字节,4 字节,8字节,所以T2和T3 的对齐系数也都是8字节, 通过unsafe.Sizeof 函数计算得到T2 和T3类型的数据大小为24字节和16字节

func main() {
    t2 := T2{}
    fmt.Println(unsafe.Sizeof(t2)) // 24 bytes
    t3 := T3{}
    fmt.Println(unsafe.Sizeof(t3)) // 16 bytes
        // 通过offset 函数来确认成员的偏移量
    fmt.Printf("t2 i8 offset:%v,i64 offset:%v,i32 offset:%v\n", unsafe.Offsetof(t2.i8), unsafe.Offsetof(t2.i64), unsafe.Offsetof(t2.i32))// 0,8,16
    fmt.Printf("t3 i8 offset:%v,i32 offset:%v,i64 offset:%v\n", unsafe.Offsetof(t3.i8), unsafe.Offsetof(t3.i32), unsafe.Offsetof(t3.i64))//0,4,8


  }

具体解析如下图所示:
以T2结构体为例,实际存储数据的只有13字节,但实际用了24字节,浪费了11个字节


T2

以T3结构体为例,实际存储数据的只有13字节,但实际用了16字节,浪费了3个字节:


T3

可以看到合理的结构体内的成员的排布可以减少对内存的消耗, 提高内存的使用效率
  • 结构体嵌套
type T4 struct {
    i32 int32
    b   bool
    t   T1
}
type T1 struct {
    i16  int16
    bool bool
}

结构体T4 嵌套T1,前面已经计算过T1类型的对齐边界为2字节, 字节长度为4字节。 通过unsafe.Sizeof和Alignof函数可得T4 类型的字节长度为12字节和对齐系数为4字节

  1. 首先来分析下T4类型的对齐系数:
    int32 的对齐系数为4字节,bool的对齐系数为1字节,T1的对齐系数为2字节

对齐系数= max{4,1,2}=4

  1. 其次分析T4 类型是如何实现内存对齐的:
    T4 内存对齐后的分布如下图所示:


    结构体嵌套

从起始地址开始前四位地址为数据i32,接下来b的对齐系数为1,所以直接接在后面,即在5号位置,接下来就是t类型数据,因为对齐系数为2, 此时在6号位置6%2==0 成立,此时t的位置就在6号位放置,6-7 号为t.i32, 8号为t.bool, 9号为t类型的pad , 因为T4的对齐系数是4, 此时T4 的总长度为9, 9%4!=0, 需要在10-11 补充pad 。最终12%4==0, 所以结构4 所占的字节长度为12

你可能感兴趣的:(golang - 内存对齐)