浏览到的一篇文章,让我也有机会反思一下 go 内存管理。网络上,go 内存管理方面的介绍挺多的,面试的时候,偶尔也会被问到内存管理。
而且,从 go1.15 到 go1.16 在 size class 上引入了新的内存块,能直观的看出来这种变化,也很有必要记录一下。
类型 size
我们先来回顾一下,go 类型的内存大小,以几个通用的数据类型举例,string 类型占用 16 字节,[]string 字符串切片占用 24 个字节,interface{} 占用 16 个字节。
为什么 interface{} 占用 16 个字节呢,因为 interface 在内存中的数据类型如下。我理解,eface 是 empty interface 的简称,表示没有方法类型。eface 包含两个成员属性,都是指针类型,而指针类型占用 8 个字节,不需要额外的 padding 内存,所以,interface{} 占用 16 个字节
type eface struct {
_type *_type
data unsafe.Pointer
}
当然,go 也提供了计算类型占用内存大小的方法,我们可以通过类型声明来直接计算出类型的尺寸大小。下面的 Sizeof 就被用来计算类型的内存大小。
这样计算出来只是类型的内存大小,实际存储数据的内存大小并不会被计算出来。实际存储数据的大小如何计算呢?
拿 []string 的类型来说明,通过 Sizeof 计算的结果是 24 字节,但实际的数据占用内从并没有被统计进去,如果 []string 的元素中包含 100字节的字符串,如何把这部分数据也计算出来呢?
目前来看,确实没有什么好的办法,只能按个元素计算内存大小了。
func main() {
var a interface{}
size := unsafe.Sizeof(a)
fmt.Println(size)
}
接口类型
接口类型包含两份数据,一份是类型数据,一份是数据本身。将[]string 类型转换赋值给 interface 类型,和赋值给[]string 类型相比,虽然变量 a 类型占用了 16 字节,变量 b 类型占用了 24 字节,但实际上,interface{} 花费了更多的内存。
主要的原因是指针类型,无论实际的元素占用多个的内存大小,只要是转换为指针类型,也都只占用 8 个字节的大小。
func main() {
src := []string{}
var a interface{} = src
var b []string = src
}
我们看一下 eface 中 _type 的类型声明,避免程序中无效的 interface 类型转换,也是提高程序性能的一种手段。
go1.16 新特性
我们先来看一个例子,使用 benchmark 性能压测 sort.Strings 性能。
sort.Strings 是字符串排序的方法,对切片元素按照增序进行排序。这个排序过程也涉及到 interface 类型的转换操作。
另外,在第一次排序完成之后,变量 s 其实就已经有序了,这个性能压测的输入并不是随机的。
var s = []string{"heart", "lungs", "brain", "kidneys", "pancreas"}
func BenchmarkName(b *testing.B) {
for i := 0; i < b.N; i++ {
sort.Strings(s)
}
}
我们首先在 go 1.16 的环境下,执行性能测试,执行3次,每次输出内存申请的信息,执行结果如下:
作为对比,我们切换到 go1.15 的环境下做同样的性能测试:
除了截图上明确标红的部分,在这个问题上,go1.16 比 go1.15 性能上有了一点点的性能提升。
下面的重点落在了 go1.16每次申请的内存是 24 B/op,而go1.15是 32 B/op。我们可以从 /runtime/sizeclasses.go 了解到个变化,
下面是 go1.15的源码
/ Code generated by mksizeclasses.go; DO NOT EDIT.
//go:generate go run mksizeclasses.go
package runtime
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
下面是go1.16的源码,比较明显,go1.16增加了 24 bytes 的 class。
每个 span 的大小是 8192,24bytes 的 class,每个 span 可以申请 341 个。剩余的 8 byte 就必须浪费了。
最大的浪费比率,只能是假设的这种情况,内存中对于 17 bytes 大小的对象,只能将它存储到 class 为3 的对象中,这就导致 24 bytes 中有 7 bytes 会被浪费掉,总共可以申请 341 个对象,总共浪费 341 *7 +8 = 2395 bytes,占比 29.24%
不过,要构造类型大小 17 bytes 的对象,在业务代码中基本不会存在,因为有内存对齐的逻辑存在。
// Code generated by mksizeclasses.go; DO NOT EDIT.
//go:generate go run mksizeclasses.go
package runtime
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 24 8192 341 8 29.24%
// 4 32 8192 256 0 21.88%
// 5 48 8192 170 32 31.52%
// 6 64 8192 128 0 23.44%
// 7 80 8192 102 32 19.07%
// 8 96 8192 85 32 15.95%
// 9 112 8192 73 16 13.56%