首页
Preview

Golang Map 的内存分配和性能优化

GOLANG

最近我在 Stack Overflow 上发现了一个有趣的问题,关于在 Golang 中使用接口作为值(map[int]interface{})与使用空结构体作为值(map[int]struct{})时,Golang maps 的内存分配问题。提问者设置了两个基准测试来比较这两种 map 类型,并发现了一些奇怪的结果。测试如下:

下面的代码中,我们有两个函数,每个函数都会创建一个 map,并重复执行一个零值赋值一定次数。也就是说,在执行结束时,map 将有固定数量的条目,每个条目将接收类型的零值(对于接口来说是 nil,对于空结构体来说是 struct{}{})。

package main

func 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 main

import "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()
    }
}

这些基准测试的结果如下所示:

goos: darwin
goarch: amd64
pkg: awesomeProject1
Benchmark_Interface-8         130419          8949 ns/op        7824 B/op          7 allocs/op
Benchmark_EmptyStruct-8       165147          6964 ns/op        3070 B/op         17 allocs/op
PASS
ok      awesomeProject1 3.122s

这两个 map 的条目都被分配了零值(nilstruct{}{},即不需要分配的值)。但是我们发现空结构体版本运行得更快,使用的内存更少,但是它却进行了更多的分配。为什么会出现这种情况呢?这两个基准测试看起来如此相似,为什么会有如此不同的结果呢?

TL;DR

Golang 中的 map 内部设计高度优化,以实现更好的性能和内存管理。map 会跟踪可以持有指针的键和值。如果 bucket 中的条目不能持有指针,map 就会创建溢出 bucket,以避免垃圾回收(GC)的不必要开销,这会导致更多的分配和更好的性能(map[int]struct{} 的情况)。

详细解释

在回答这个问题之前,我们需要了解 map 的初始化和 map 结构。我们将先介绍这些主题,然后分析一些基准测试,以了解我们在这里尝试理解的两种 map 类型的性能。我创建了一个存储库,其中包含一些测试,以帮助理解本文的答案。因此,如果你在文本中看到对测试或基准测试的引用,它可能在存储库中。

Map 的初始化

map 有两种初始化方法:

  • make(map[int]string):当我们不知道要添加到 map 中的条目数量时使用。
  • make(map[int]string, hint):当我们有一个要添加到 map 中的条目数量的想法时使用。hint 是 map 的初始容量的估计值。

无论我们选择哪种初始化方法,map 都是可变的,并且会根据需要增长。但是,第二种方法为至少 hint 个条目预分配内存,这会提高性能。

Map 的结构

在 Go 中,map 是一个哈希表,将其键值对存储到 bucket 中。每个 bucket 是一个数组,最多可容纳 8 个条目。默认的 bucket 数量为 1。当每个 bucket 中的条目数量达到平均负载(也称为负载因子)时,map 将通过将 bucket 数量加倍来变大。每次 map 增长时,都会为新条目分配内存。实际上,每当 bucket 的负载达到 6.5 或更高时,map 就会增长。这个值是硬编码的,并且被选择以优化内存使用。

在幕后,map 是指向 hmap 结构的指针。还有 map 结构,它包含一些有关 map 类型的信息。Golang maps 的源代码可以在此处找到:

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

正如你在上面的链接中所看到的,map 的内部结构很复杂。在下面的链接中,你可以找到一些关于如何“黑掉”map类型的见解。Aleksandr Kochetkov 做了一个非常好的工作,展示了一些 map 内部的细节。

https://hackernoon.com/some-insights-on-maps-in-golang-rm5v3ywh

https://play.golang.org/p/NaoC8fkmy9x

需要注意的一件重要事情是,maps 会跟踪可以持有指针的键和值。如果 bucket 中的条目不能持有任何指针,则将标记该 bucket 为不包含指针,并且 map 将创建溢出 bucket(这意味着更多的内存分配)。这避免了 GC 的不必要开销。请参见这个结构体中的评论(第 132 行),以及这个帖子 作为参考。

基准测试分析

空结构体 struct{} 没有字段,也不能持有任何指针。因此,在空结构体的情况下,bucket 将被标记为 不包含指针 。这将避免在 map 中进行不必要的扫描,我们可以期望获得更好的执行速度。此外,我们还可以期望 map[int]struct{} 类型的 map 进行更多的内存分配,因为它在增长时会创建更多的溢出 bucket。interface{}类型与空结构体不同,它可以包含任何值,包括指针。为了理解这对映射性能的影响,我们需要了解映射桶如何跟踪所有指针的内存前缀大小(ptrdata 字段,第33行)。能够保存指针的映射类型不会分配额外的溢出桶。但是,这些类型需要被GC扫描。

在映射的内部实现中,我们可以看到ptrdata字段如何用于决定是否创建更多的溢出桶(map.go,第265行)。请参考此链接,查看map[int]struct{}map[int]interface{}的所有指针占用的内存前缀大小。

当我们查看CPU分析时,Benchmark_InterfaceBenchmark_EmptyStruct之间的差异就显而易见了。Benchmark_Interface没有(*hmap)createOverflow方法,因此会导致额外的内存分配流程。

Benchmark_EmptyStruct CPU profile

Benchmark_EmptyStruct CPU profile (png, svg)

Benchmark_Interface CPU profile

Benchmark_Interface CPU profile (png, svg)

Benchmarks Results

我定制了基准测试,以传递条目数和映射的初始容量(hint)。以下是执行结果。当条目很少或初始容量大于条目数时,结果基本相同。如果有许多条目和初始容量为0,则会得到相当不同的分配数字。

随着条目数量的增加,当我们比较InterfaceEmptyStruct时,我们清楚地看到速度上的差异。Interface基准测试比EmptyStruct基准测试慢得多。我们还可以看到,当映射的初始容量大于条目数时,映射不需要太多分配,因为为所有条目预先分配了空间。

Conclusion

导致Stack Overflow上的问题的不同结果与性能和内存管理的映射优化有关。类型为map[int]interface{}的映射较慢,因为它们在GC扫描可以保存指针的桶时会出现性能下降。类型为map[int]struct{}的映射使用的内存较少,因为它们实际上使用的内存较少 😄。不需要为空结构体分配空间(Test_EmptyStructValueSize表明struct{}{}大小为零)。尽管nilinterface{}的零值,但interface{}类型需要一些空间来存储任何值(TestNilInterfaceValueSize测试显示存储nil值的interface{}类型的大小不为零)。最后,空结构体基准测试分配更多,因为map[int]struct{}类型需要更多的溢出桶(为了性能优化),因为其桶不保存任何指针。

译自:https://levelup.gitconnected.com/memory-allocation-and-performance-in-golang-maps-b267b5ad9217

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论