解析struct的内存布局

解析struct的内存布局

在平时开发过程中,我们常用map[string]struct{}来实现一个Set,用struct{}的原因是struct{}不占用内存空间,为什么空struct会不占用内存空间?对于自定义的struct的内存空间的占用是什么样的?

struct的大小

struct和java中的对象类似,在内存中都是一块连续的空间,在java中,对象的引用其实就是一个对象在堆内存的内存地址起点,当访问第N个对象时,就是访问 (起点地址 + 第N个对象的地址偏移量)位置,在go中原理也类似,go struct的大小就是由struct中的属性决定的,例如:

type A struct {
    x int8
    y int8
    z int8
}
var a A
fmt.Println(unsafe.Sizeof(a)) // 3

可以看到输出结果:3,表示一个a对象在内存中占用3个字节,此时的内存结构如下:
解析struct的内存布局

struct的内存对齐

我们稍微修改一下字段的类型,然后再看下结果

type B struct {
    x int8
    y int32
    z int8
}
var b B
fmt.Println(unsafe.Sizeof(b)) // 12

这里输出结果会是8,按上面的理解int32占用4个字节,再加上两个1字节的int8,应该输出6才对,这个就引出go中的内存对齐机制了:
在计算机中,cpu在访问内存时,每次访问的并不是一个字节,而是一个字(word),一个字的字长由cpu的位数决定,例如32位的cpu一个字长位32bit,也就是4个字节,64位的cpu一个字长64bit,也就是8个字节,go为了减少在访问对象时cpu与内存的交互次数,会在编译时按照一定的规则进行内存对齐,防止访问一个对象的属性需要经历两次总线周期。
假如上面的B对象如果是6字节,则内存结构如下:
解析struct的内存布局
看上图,此时的cpu字长位1字节,那么访问y对象需要cpu需要和内存打两次交道
所以go中进行内存对齐之后内存结构如下:
解析struct的内存布局
黄色部分为内存对齐部分,这样在访问属性y时,cpu就只需要对内存进行一次读取了。

struct的对齐规则

内置unsafe包的Sizeof函数用来获取一个变量的大小,另外还有内置unsafe包的Alignof函数可以来获取一个变量的对齐系数,例如:

var a A
fmt.Println(unsafe.Alignof(a.x)) // 1
fmt.Println(unsafe.Alignof(a.y)) // 4
fmt.Println(unsafe.Alignof(a.z)) // 1
fmt.Println(unsafe.Alignof(a)) // 4

具体规则如下:

  • 对于任意类型的变量 x ,unsafe.Alignof(x) 至少为 1;
  • 对于 struct 类型的变量 x,计算 x 每一个字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值;
  • 对于 array 类型的变量 x,unsafe.Alignof(x) 等于构成数组的元素类型的对齐系数;
    对齐系数必须是对象大小的整数倍,也就是说:在go中,对于任意对象a,等式unsafe.Sizeof(a) = X * unsafe.Aligof(a)必定成立
    那有了这个等式,上面的对象B,X最小应该等于2,但是因为unsafe.Sizeof(b) = 12,所以X是等于3的,我们只需要调整一下字段顺序就可以将对象B占用的空间大小优化成8
type C struct {
    x int8
    z int8
    y int32
}
var c C
fmt.Println(unsafe.Sizeof(b)) // 8

此时的内存结构如下:
解析struct的内存布局
所以通过这个例子可以看出,合理的编排对象中属性的位置,可以减少对象的内存大小

嵌套struct

有了上面的基础,可以推断一下嵌套结构体和切片,数组的unsafe.Sizeof,unsafe.Aligniof大小,来练习一下

type D struct {
	x int
	a A
	b B
	arr [5]A
	sli []A
}

var d D
fmt.Println(unsafe.Alignof(d)) // 8
fmt.Println(unsafe.Sizeof(d)) // 64
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.x)) // 8
fmt.Println(unsafe.Sizeof(d.x)) // 8
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.a)) // 1
fmt.Println(unsafe.Sizeof(d.a)) // 3
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.b)) // 4
fmt.Println(unsafe.Sizeof(d.b)) // 12
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.arr)) // 1
fmt.Println(unsafe.Sizeof(d.arr)) // 15
fmt.Println("---------")
fmt.Println(unsafe.Alignof(d.sli)) // 8
fmt.Println(unsafe.Sizeof(d.sli)) // 24

其中a + b 做了内存对齐,填充了两个字节,arr也做了内存对齐,填充了一个字节
这里为什么切片是24个字节呢,这个和切片的结构体有关,在runtime/slice.go中有对slice的定义:
解析struct的内存布局
这里的len和cap在64位的cpu上都是占用8个字节的,array是一个指向底层数组的指针,也是占用8个字节,所以加起来一共就是24个字节。

上一篇:Go语言学习记录2——基础语法【包、变量、函数】


下一篇:golang速成教程