实验
我们可以直接使用unsafe.Sizeof(foo)来获取某个结构体变量在内存中占用的大小, 看一下下边的结构体,思考一下他们在内存中占用的大小一样么?
1 | type Foo0 struct { |
实验结果
1 | f0 := Foo0{} |
可能有的人会问,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,其他颜色为其他的变量
如果CPU需要用到a:
- 非对齐情况: 先读取前4个字节,再读取后4个字节,将2-5进行拼接得到a
- 对齐情况: 直接读出后4个字节就能得到a,注意:空白格是为了对齐而填充的内存占位
两种情况哪个更有效率一目了然,典型的空间换时间,提升性能
golang的对齐规则(结构体)
变量定义: 编译器默认对齐大小为N,当前成员变量类型的大小为n,所有成员类型最大大小为mn
- 第一个成员变量的偏移量为 0。之后的每个成员偏移量取N或n小的那个数的整数倍
- 结构体本身,所有成员排列完后,结构体整体偏移量取N或mn小的那个数的整数倍
使用unsafe.Sizeof(x) 获取当前变量的内存大小
应用
回到上边的实验题,我的机器是64位的,对于f0和f1的内存结构分别应该入下图所示
- 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 | type Foo2 struct { |
意义
提高内存利用率,合理安排结构体的字段顺序,可以降低内存的使用,提升内存利用率,如f2和f3
提高CPU的读取效率,比如前边的f0和f1,既降低了内存还提升了读取效率
如果你的系统是高并发系统,并且对内存使用比较敏感(内存要钱),你需要好好考虑一下你的结构体设计,尤其是在过程中需要产生很多这类的结构体变量。
但是如果你的系统很长一段时间根本没有并发、内存的顾虑,而重要的是要优先上线使用,那就要优先考虑开发效率,已功能为主。
因地制宜,因时而变。