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_Interface
Benchmark_Interface
(*hmap) createOverflow
Benchmark_EmptyStruct CPU 配置文件
Benchmark_EmptyStruct CPU 配置文件 [ png , svg ]
Benchmark_Interface CPU 配置文件
Benchmark_Interface CPU 配置文件 [ png , svg ]
我自定义了测试以通过条目数和地图的初始容量(提示)。以下是处决的结果。当条目较少或初始容量大于条目数时,结果基本相同。如果您有许多条目且初始容量为 0,您将获得完全不同的分配数。
基准 | 参赛作品 | 初始容量 | 速度 | 字节/操作 | 分配/操作 |
---|---|---|---|---|---|
空结构 | 7 | 0 | 115 纳秒 / 运算 | 0 B/操作 | 0 分配/操作 |
界面 | 7 | 0 | 94.8 纳秒/运算 | 0 B/操作 | 0 分配/操作 |
空结构 | 8 | 0 | 114 纳秒 / 运算 | 0 B/操作 | 0 分配/操作 |
界面 | 8 | 0 | 110 纳秒 / 运算 | 0 B/操作 | 0 分配/操作 |
空结构 | 9 | 0 | 339 纳秒/操作 | 160 元/运 | 1 分配/操作 |
界面 | 9 | 0 | 439 纳秒/操作 | 416 B/OP | 1 分配/操作 |
空结构 | 16 | 16 | 444 纳秒/运算 | 324 B/OP | 1 分配/操作 |
界面 | 16 | 16 | 586 纳秒/运算 | 902 B/OP | 1 分配/操作 |
空结构 | 16 | 32 | 448 纳秒/运算 | 640 机/运 | 1 分配/操作 |
界面 | 16 | 32 | 724 纳秒 / 运算 | 1792 年 B/OP | 1 分配/操作 |
空结构 | 16 | 100 | 634 纳秒/运算 | 1440 乙/运 | 2个分配/操作 |
界面 | 16 | 100 | 1241 纳秒/运算 | 4128 B/OP | 2个分配/操作 |
空结构 | 100 | 0 | 5339 纳秒/操作 | 3071 B/on | 17 个分配/操作 |
界面 | 100 | 0 | 6524 纳秒/运算 | 7824 B/OP | 7个分配/操作 |
空结构 | 100 | 128 | 2665 纳秒/运算 | 3109 B/OP | 2个分配/操作 |
界面 | 100 | 128 | 3938 纳秒/操作 | 8224 B/OP | 2个分配/操作 |
结论
基准测试方法没有任何问题。这都与性能和内存管理的映射优化有关。基准显示每次迭代的平均值。类型的映射map[int]interface{}
比较慢,因为当 GC 扫描可以保存指针的存储桶时,它们会遭受性能下降。类型的映射map[int]struct{}
使用更少的内存,因为它们实际上使用更少的内存(Test_EmptyStructValueSize
显示struct{}{}
大小为零)。尽管nil
是 的零值interface{}
,但这种类型需要一些空间来存储 ANY 值(Test_NilInterfaceValueSize
测试显示interface{}
持有nil
值的大小不为零)。最后,空结构基准分配更高,因为类型map[int]struct{}
需要更多的溢出桶(用于性能优化),因为它的桶不包含任何指针。
- 1 回答
- 0 关注
- 245 浏览
添加回答
举报