https://futures.yunsonbai.top/?hmsr=yunsonbai.top
贵金属行情

Go巧用内存对齐-让结构体内存更小

https://futures.yunsonbai.top/?hmsr=yunsonbai.top

实验

我们可以直接使用unsafe.Sizeof(foo)来获取某个结构体变量在内存中占用的大小, 看一下下边的结构体,思考一下他们在内存中占用的大小一样么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Foo0 struct {
a int8
b int32
c int16
d int8
e bool
}

type Foo1 struct {
a int8
b int32
e bool
c int16
d int8
}

实验结果

1
2
3
4
f0 := Foo0{}
f1 := Foo1{}
fmt.Println("f0 size:", unsafe.Sizeof(f0)) // 将会输出12
fmt.Println("f1 size:", unsafe.Sizeof(f1)) // 将会输出16

可能有的人会问,12?我预计的是1 + 4 + 2 + 1 + 1 = 9,12怎么可能,更别说16了。别急慢慢往下分析

内存对齐

为了迎合CPU一块一块的读取内存内容,将内存补足。32位机一般是以每4个字节去内存读取数据,也就是说如果我们的数据占用1个字节,CPU读取这个数据时,也是可能需要读取4个字节出来(需要看具体场景和位置)。(64位机是8个字节)

编译器默认对齐大小: 32位机是4字节,64位机是8字节

为什么对齐

先看一下这个例子,为了好画以4个字节为单位去读取,a类型为int32,刚好a在内存的位置为下图,蓝色为a,其他颜色为其他的变量

yunsonbai.top-内存对齐与非对齐对CPU的影响

如果CPU需要用到a:

  • 非对齐情况: 先读取前4个字节,再读取后4个字节,将2-5进行拼接得到a
  • 对齐情况: 直接读出后4个字节就能得到a,注意:空白格是为了对齐而填充的内存占位

两种情况哪个更有效率一目了然,典型的空间换时间,提升性能

golang的对齐规则(结构体)

变量定义: 编译器默认对齐大小为N,当前成员变量类型的大小为n,所有成员类型最大大小为mn

  1. 第一个成员变量的偏移量为 0。之后的每个成员偏移量取N或n小的那个数的整数倍
  2. 结构体本身,所有成员排列完后,结构体整体偏移量取N或mn小的那个数的整数倍

使用unsafe.Sizeof(x) 获取当前变量的内存大小

应用

回到上边的实验题,我的机器是64位的,对于f0和f1的内存结构分别应该入下图所示

yunsonbai.top-结构体内存分析

  • f0: 写b时击中了1号规则,需要给a后边填充3个空白内存位,写c时刚好ab占用位8,是2的整倍数,不用填充空白位,同理d,e也不需要填充空白位,最后f0的整体大小是12,刚好是abcde中最大的b(4个字节,比8小,以4为准)整倍数,不用再填充。
  • f1: 写b时击中了1号规则,需要给a后边填充3个空白内存位,写e时,ab刚好占用8,是1的整倍数,写c时,abe占用9,不满足是2的整倍数,所以在e后填充1个空白位,写入d时,满足1的整倍数不用填充, 最后f1占用13个字节,不满足abcde中最大的b的整倍数,所以末尾填充3个空白位,达到16
  • 如果机器是32位的话比较: f0 和 f1哪个的读取效率高,f0需要3次读完,f1则需要4次

如果现在有一个f2,如下,占用多少内存呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Foo2 struct {
a int8
b int16
c bool
}
f2 := Foo2{} // 答案6


type Foo3 struct {
a int8
c bool
b int16
}
f3 := Foo3{} // 答案4,非常可观,节省了33%的内存

意义

  • 提高内存利用率,合理安排结构体的字段顺序,可以降低内存的使用,提升内存利用率,如f2和f3

  • 提高CPU的读取效率,比如前边的f0和f1,既降低了内存还提升了读取效率

如果你的系统是高并发系统,并且对内存使用比较敏感(内存要钱),你需要好好考虑一下你的结构体设计,尤其是在过程中需要产生很多这类的结构体变量。

但是如果你的系统很长一段时间根本没有并发、内存的顾虑,而重要的是要优先上线使用,那就要优先考虑开发效率,已功能为主。

因地制宜,因时而变。

yunsonbai wechat
公众号:技术and生活