为了账号安全,请及时绑定邮箱和手机立即绑定

map[int]interface{} 与 map[int]struct{} 的内存分配

map[int]interface{} 与 map[int]struct{} 的内存分配

Go
慕桂英4014372 2022-07-18 17:19:37
我们和一位同事讨论了使用映射作为列表的性能,并比较了使用接口作为 value( map[int]interface{}) 与空 struct() 的性能map[int]struct{}),我们在基准测试中得到了一些奇怪的值。代码package mainfunc main() {}func MapWithInterface() {    m := map[int]interface{}{}    for i := 1; i <= 100; i++ {        m[i] = nil    }}func MapWithEmptyStruct() {    m := map[int]struct{}{}    for i := 1; i <= 100; i++ {        m[i] = struct{}{}    }}测试package mainimport "testing"func Benchmark_Interface(b *testing.B) {    for i := 0; i < b.N; i++ {        MapWithInterface()    }}func Benchmark_EmptyStruct(b *testing.B) {    for i := 0; i < b.N; i++ {        MapWithEmptyStruct()    }}结果go version go1.15.5 darwin/amd64go test -bench=. -benchmemgoos: darwingoarch: amd64pkg: awesomeProject1Benchmark_Interface-8         130419          8949 ns/op        7824 B/op          7 allocs/opBenchmark_EmptyStruct-8       165147          6964 ns/op        3070 B/op         17 allocs/opPASSok      awesomeProject1 3.122s问题因此,空结构版本似乎运行得更快,使用的内存更少,但分配更多,无法弄清楚原因。有谁知道为什么会这样?
查看完整描述

1 回答

?
慕慕森

TA贡献1856条经验 获得超17个赞

我创建了一个包含一些测试的存储库,以帮助理解这个答案。

TL;博士

Golang 中地图的内部设计针对性能和内存管理进行了高度优化。映射跟踪可以保存指针的键和值。如果桶中的条目不能保存指针,map 只是创建溢出桶以避免不必要的 GC 开销,这会导致更多的分配(的情况map[int]struct{})。

长答案

我们需要了解地图初始化和地图结构。然后,我们将分析基准。

地图初始化

地图初始化有两种方法:

  • make(map[int]string)当我们不知道将添加多少条目时。

  • make(map[int]string, hint)当我们知道要添加多少条目时。hint是对初始容量的估计。

地图是可变的,无论我们选择哪种初始化方法,它们都会按需增长。但是,第二种方法至少为hint条目预分配内存,从而提高了性能。

地图结构

Go 中的 map 是一个哈希表,将其键/值对存储到存储桶中。每个存储桶是一个数组,最多可容纳 8 个条目。桶的默认数量是 1。一旦每个桶中的条目数达到桶的平均负载(也称为负载因子),通过将桶的数量增加一倍,地图就会变得更大。每次地图增长时,它都会为新来的条目分配内存。在实践中,每当桶的负载达到 6.5 或更多时,map 就会增长。

在幕后,映射是指向结构的指针hmap。还有一个maptype结构,它保存了一些关于map. 地图的源代码可以在这里找到:

https://github.com/golang/go/blob/master/src/runtime/map.go

您可以在下面找到有关如何破解map类型以及如何查看地图增长的一些见解:

需要注意的一件重要事情是,映射会跟踪可以保存指针的键和值。如果桶中的条目不能保存任何指针,则桶被标记为不包含指针,并且映射只是创建溢出桶(这意味着更多的内存分配)。这避免了 GC 的不必要开销。请参阅结构中的此评论mapextra(第 132 行)和此帖子以供参考。

基准

空结构struct{}没有字段,也不能保存任何指针。结果,空结构案例中的存储桶将被标记为不包含指针map[int]struct{},并且随着类型映射的增长,我们可以期待更多的内存分配。另一方面,interface{}可以保存任何值,包括指针。映射桶跟踪保存指针的内存前缀的大小(ptrdata字段,第 33 行)以确定是否将创建更多溢出桶(map.go,第 265 行)。请参阅此链接以查看保存所有指针的内存前缀的大小map[int]struct{}map[int]interface{}

当我们看到 CPU 配置文件时,这两个基准测试 (Benchmark_EmptyStruct和)之间的区别很明显。没有导致额外内存分配流程的方法:Benchmark_InterfaceBenchmark_Interface(*hmap) createOverflow

Benchmark_EmptyStruct CPU 配置文件

//img1.sycdn.imooc.com//62d525cc00016e5406560777.jpg

Benchmark_EmptyStruct CPU 配置文件 [ png , svg ]

Benchmark_Interface CPU 配置文件

//img1.sycdn.imooc.com//62d525d900013d4f04180962.jpg

Benchmark_Interface CPU 配置文件 [ png , svg ]

我自定义了测试以通过条目数和地图的初始容量(提示)。以下是处决的结果。当条目较少或初始容量大于条目数时,结果基本相同。如果您有许多条目且初始容量为 0,您将获得完全不同的分配数。

基准参赛作品初始容量速度字节/操作分配/操作
空结构70115 纳秒 / 运算0 B/操作0 分配/操作
界面7094.8 纳秒/运算0 B/操作0 分配/操作
空结构80114 纳秒 / 运算0 B/操作0 分配/操作
界面80110 纳秒 / 运算0 B/操作0 分配/操作
空结构90339 纳秒/操作160 元/运1 分配/操作
界面90439 纳秒/操作416 B/OP1 分配/操作
空结构1616444 纳秒/运算324 B/OP1 分配/操作
界面1616586 纳秒/运算902 B/OP1 分配/操作
空结构1632448 纳秒/运算640 机/运1 分配/操作
界面1632724 纳秒 / 运算1792 年 B/OP1 分配/操作
空结构16100634 纳秒/运算1440 乙/运2个分配/操作
界面161001241 纳秒/运算4128 B/OP2个分配/操作
空结构10005339 纳秒/操作3071 B/on17 个分配/操作
界面10006524 纳秒/运算7824 B/OP7个分配/操作
空结构1001282665 纳秒/运算3109 B/OP2个分配/操作
界面1001283938 纳秒/操作8224 B/OP2个分配/操作

结论

基准测试方法没有任何问题。这都与性能和内存管理的映射优化有关。基准显示每次迭代的平均值。类型的映射map[int]interface{}比较慢,因为当 GC 扫描可以保存指针的存储桶时,它们会遭受性能下降。类型的映射map[int]struct{}使用更少的内存,因为它们实际上使用更少的内存(Test_EmptyStructValueSize显示struct{}{}大小为零)。尽管nil是 的零值interface{},但这种类型需要一些空间来存储 ANY 值(Test_NilInterfaceValueSize测试显示interface{}持有nil值的大小不为零)。最后,空结构基准分配更高,因为类型map[int]struct{}需要更多的溢出桶(用于性能优化),因为它的桶不包含任何指针。


查看完整回答
反对 回复 2022-07-18
  • 1 回答
  • 0 关注
  • 245 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信