go 语言 结构体在内存中的布局

一些基础知识

  • 字节对齐
  • unsafe.Sizeof
  • unsafe.Offsetof
  • 内存空洞

字节对齐

可以使计算机在加载和保存数据时,更加的有效率

通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节),其它的类型对齐到机器字大小

unsafe.Sizeof

返回操作数在内存中的字节大小,参数可以是任意类型的表,但不会对表达式进行求值(不求值也能知道大小,好神奇呀)

unsafe.Sizeof 返回的大小只包含数据结构中固定的部分。如果结构体含有指针字段,不包括针指向的内容。Go语言中非聚合类型通常有一个固定的大小,而聚合类型没有固定的大小,比如 结构体类型和数组类型

unsafe.Offsetof

函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,包括可能的空洞

内存空洞

一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大。因为可能存在内存空洞,内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐。内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响

结构体内存布局

设:机器字大小为8个字节

产生的空洞

var x struct {
  a bool
  b int16
  c []int
}
/* output
Sizeof(x)   = 32  Alignof(x)   = 8
Sizeof(x.a) = 1   Alignof(x.a) = 1 Offsetof(x.a) = 0
Sizeof(x.b) = 2   Alignof(x.b) = 2 Offsetof(x.b) = 2
Sizeof(x.c) = 24  Alignof(x.c) = 8 Offsetof(x.c) = 8
*/
  • x 占用内存大小为 32字节
  • x.c字段是一个切片,占用24个字节(3个机器字),c.data, c.len, c.cap 分别用 8个字节(1 个机器字)
  • x.a + x.b 总共占用 3字节。用x的占用总字节数 32 - (1 + 2 + 24) = 5, 说明有5字节的内存空洞
  • 由于x.c占用了3个机器字,所以空洞不是它产生的
  • x.a + x.b = 3字节,不满一个机器字(8字节),所以a和b之间,b和c之间产生了总共5字节的空洞

字段偏移分析

  • Offsetof(x.a) = 0 说明 字段 a 处在结构的起始处,a与结构体起始处没有偏移(与起始处没有空洞)
  • Offsetof(x.b) = 2 说明 字段 b 相对于结构体 起始处 偏移了 2 字节,而Sizeof(x.a) = 1说明 x.a 占用只占用了1字节,但 b 偏移了 2 字节,说明b与a之间有 1 字节的空洞,否则 b 只应该偏移 1 字节,即x.a的大小。
  • 那么 a与b之间,总共是4字节的大小: x.a 1字节 + 空洞 1 字节 + x.b 2字节
  • 如果 x.c 与 x.b之前没有空洞,则x.c只应该偏移4字节,但实际却偏移了8字节,则 说明 x.c 与 x.b 之间 存在 8 - 4 = 4 字节的空洞
  • 所以 x 结构体的内存 分布是: x.a(1)____空洞(1)____x.b(2)____空洞(4)____x.c(24)
  • 对齐方式:按一个机器字对齐的

layout of struck

结构体字段顺序

Go 语言中,结构内部字段的声明顺序和它们在内存中的顺序可能是不一样的。一个编译器可以随意地重新排列每个字段的内存位置,有效的包装可以使数据结构更加紧凑,从而节省内存空间

内存占用

不同结构体相同字段占用内存大小也会不一样,虽然 s1,s2,s3 有着相同的字段,但s1占用了较多的内存空间

var s1 = struct {a bool;b float64;c int16}{} // 3 words
var s2 = struct {a float64;b int16;c bool}{} // 2 words
var s3 = struct {a bool;b int16;c float64}{} // 2 words

s1占用空间

sizeof(s1) =   24 Alignof(s1) =    8
Sizeof(s1.a) =  1 Alignof(s1.a) =  1 Offsetof(s1.a) =  0
Sizeof(s1.b) =  8 Alignof(s1.b) =  8 Offsetof(s1.b) =  8
Sizeof(s1.c) =  2 Alignof(s1.c) =  2 Offsetof(s1.c) =  16

综上: s1.a 与 s1.b 之间有 7 字节 的空洞,s1.c与结构体结束处(尾部)有 6 字节的空洞

所以: s1 总字节数是 1 + 8 + 2 + (7 + 6) 空洞 = 24 byte,即3个机器字,可以看出 s1 的字段与字段之间,排列的并不是很紧凑,有较大空洞,造成了内存的浪费

对齐方式:按一个机器字对齐的

|-a-|----------holes------------| 8字节,即一个机器字
|---------------b---------------| 8字节,即一个机器字
|---c---|---------holes---------| 8字节,也可看出是按一个机器字对齐的

s2 占用空间

sizeof(s2) =   16 Alignof(s2) =    8
Sizeof(s2.a) =  8 Alignof(s2.a) =  8 Offsetof(s2.a) =  0
Sizeof(s2.b) =  2 Alignof(s2.b) =  2 Offsetof(s2.b) =  8
Sizeof(s2.c) =  1 Alignof(s2.c) =  1 Offsetof(s2.c) =  10

综上: s2.a 的大小是一个机器字,本身就是对齐的,且是所有字段中长度最大的,与 s2.b之间没有空洞,s2.c紧贴s2.b,它们之间也没有空洞,s2.c与结构体结束处(尾部)有 5(8-2+1) 字节的空洞

所以: s2 总字节数是 8 + 2 + 1 + (5) 空洞 = 16 byte,即2个机器字,可以看出 s2 的字段与字段之间,排列是很紧凑,可以大大节省内存空间

对齐方式:按一个机器字对齐的

|---------------a---------------| 8字节,即一个机器字
|---b---|-c-|-------holes-------| 8字节,即一个机器字

s3占用空间

sizeof(s3) =   16 Alignof(s3) =    8
Sizeof(s3.a) =  1 Alignof(s3.a) =  1 Offsetof(s3.a) =  0
Sizeof(s3.b) =  2 Alignof(s3.b) =  2 Offsetof(s3.b) =  2
Sizeof(s3.c) =  8 Alignof(s3.c) =  8 Offsetof(s3.c) =  8

对齐方式:按一个机器字对齐的

s3 布局与 s2 相似,可以看成是上下两层对调了,但排列是很紧凑的,也是2个机器字

|-a-|---b---|-------holes-------| 8字节,即一个机器字
|---------------c---------------| 8字节,也可看出是按一个机器字对齐的

相关问题

未来的Go语言编译器应该会默认优化结构体的顺序,当然应该也能够指定具体的内存布局,相同讨论请参考 Issue10014