分析原因,map 是通过 key 和 hash 值来分布和查找对象。map 不会收缩「不再使用」的空间,即使把 map 中的键值对,它依然保留内存空间继续使用。
一、正确的使用姿势
预估 map 容量
性能测试: map_test.go
package test
import (
"testing"
)
func test(m map[int]int) {
for i := 0; i < 10000; i++ {
m[i] = i
}
}
func BenchmarkMap(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := make(map[int]int) //不带容量的初始化
test(m)
}
}
func BenchmarkMapCap(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := make(map[int]int, 10000) //带容量的初始化
test(m)
}
}
执行go test -v -bench=. -benchmem的结果:
BenchmarkMap-4 1000 1254931 ns/op 687227 B/op 276 allocs/op
BenchmarkMapCap-4 2000 567847 ns/op 322250 B/op 11 allocs/op
struct VS *struct「值 vs 指针」
性能测试:mapStruct_test.go
package test
import (
"testing"
)
type User struct {
name string
age int
}
func test(m map[int]User) {
for i := 0; i < 10000; i++ {
user := User{
name: "小明",
age: i,
}
m[i] = user
}
}
func testPointer(m map[int]*User) {
for i := 0; i < 10000; i++ {
user := User{
name: "小明",
age: i,
}
m[i] = &user
}
}
func BenchmarkStruct(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := make(map[int]User)
test(m)
}
}
func BenchmarkStructPointer(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := make(map[int]*User, 10000)
testPointer(m)
}
}
执行go test -v -bench=. -benchmem的结果:
BenchmarkStruct-4 1000 1678196 ns/op 1274963 B/op 212 allocs/op
BenchmarkStructPointer-4 1000 1382258 ns/op 639520 B/op 10002 allocs/op
1
2
结论
带有容量的 map 初始化,可以有效的减少内存分配的次数,进而减少每次操作的耗时。
内置类型用值,构造的 struct 类型用指针
指针类型会影响 golang. gc 的速度
二、你以为你以为的就是你以为的
清空 map 不等于释放内存
内存分配测试: main.go
package main
import (
"fmt"
"runtime"
)
var intMap map[int]int
var cnt = 8192
func initMap() {
intMap = make(map[int]int, cnt)
for i := 0; i < cnt; i++ {
intMap[i] = i
}
}
func printMemStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("HeapAlloc = %v HeapIdel= %v HeapSys = %v HeapReleased = %v\n", m.HeapAlloc/1024, m.HeapIdle/1024, m.HeapSys/1024, m.HeapReleased/1024)
}
func main() {
//程序启动占用内存
printMemStats()
//map 第一次初始化
initMap()
runtime.GC()
printMemStats()
fmt.Printf("map len's %d\n", len(intMap))
for i := 0; i < cnt; i++ {
//delete 所有 key
delete(intMap, i)
}
fmt.Printf("map len's %d\n", len(intMap))
runtime.GC()
printMemStats()
//map 置为nil
intMap = nil
runtime.GC()
printMemStats()
//map 第二次初始化
initMap()
runtime.GC()
printMemStats()
}
程序使用 runtime.ReadMemStats() 函数来获取堆的使用信息。它打印四个值:
HeapSys:程序向应用程序申请的内存
HeapAlloc:堆上目前分配的内存
HeapIdle:堆上目前没有使用的内存
HeapReleased:回收到操作系统的内存
程序运行结果分析:
HeapAlloc = 45 HeapIdel= 552 HeapSys = 768 HeapReleased = 0 -> 程序启动占用内存
HeapAlloc = 358 HeapIdel= 192 HeapSys = 736 HeapReleased = 0 -> map 第一次初始化
map len's 8192
map len's 0
HeapAlloc = 358 HeapIdel= 192 HeapSys = 736 HeapReleased = 0 -> delete 所有 key
HeapAlloc = 46 HeapIdel= 512 HeapSys = 736 HeapReleased = 0 -> map 置为 nil
HeapAlloc = 358 HeapIdel= 192 HeapSys = 736 HeapReleased = 0 -> map 第二次初始化
结论
删除 map 中的所有 key,map 占用内存仍处于「使用状态」, map 置为 nil,map 占用的内存处于「空闲状态」。
处于空闲状态内存,一定时间内在下次申请的可重复被使用,不必再向操作系统申请。
笔者使用的是 go 1.9.7 版本
三、sync.Map 了解一下
产生背景
在对标准库做额外的审查和性能分析之后,Go 团队成员发现当使用 sync.RWMutex 的代码被部署在「很多核 」的 CPU 上高并发读的场景下,它的性能远低于理想值。所以使用 sync.RWMutex 封装的数据结构中读取数据的性能会受很大影响。
注意: 在2017 GopherCon有一个闪电演讲:An overview of sync.Map介绍了关于sync.Map诞生的原因以及它的设计目标。如果你在考虑使用这个实现,建议你一定要看下这个视频,视屏中讲解了它可能有的一些性能陷阱。
适用场景
sync.Map 类型是针对两种常见用例进行优化的:
当给定的 key 只写入一次但读多次的时候,比如不断增长的缓存
当多个 goroutines 读取、写入和覆盖 map 不相交的键集合(我理解就是每个 goroutine 只负责部分 key 的读取、写入和覆盖)
笔者翻译水平有限,原文如下:
The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjoint sets of keys. In these two cases, use of a Map may significantly reduce lock contention compared to a Go map paired with a separate Mutex or RWMutex.
map VS sync.Map 压测
笔者模拟后端 map 的使用方式——多个 goroutine 对同一 map 同时执行读取、写入和删除的操作,预计map 的性能要比 sync.Map 的性能要好,但是发现 sync.Map 性能略胜一筹,原因不明,先记录在此处。
性能测试:syncMap_tset.go
package test
import (
"sync"
"testing"
)
type Map struct {
m map[int]int
sync.RWMutex
}
type SMap struct {
sm sync.Map
}
func (m *Map) Insert(i int, s int, wg *sync.WaitGroup) {
m.Lock()
m.m[i] = s
m.Unlock()
wg.Done()
}
func (sm *SMap) Insert(i int, s int, wg *sync.WaitGroup) {
sm.sm.Store(i, s)
wg.Done()
}
func (m *Map) Get(i int, wg *sync.WaitGroup) (s int) {
defer wg.Done()
m.RLock()
s, ok := m.m[i]
if ok {
m.RUnlock()
return s
}
m.RUnlock()
return 0
}
func (sm *SMap) Get(i int, wg *sync.WaitGroup) (s int) {
defer wg.Done()
v, ok := sm.sm.Load(i)
if ok {
return v.(int)
}
return 0
}
func (m *Map) Delete(i int, wg *sync.WaitGroup) {
m.Lock()
delete(m.m, i)
m.Unlock()
wg.Done()
}
func (sm *SMap) Delete(i int, wg *sync.WaitGroup) {
sm.sm.Delete(i)
wg.Done()
}
func operateMap(m *Map, work int) {
wg := sync.WaitGroup{}
wg.Add(work*2 + 3)
go func() {
defer wg.Done()
for i := 0; i < work; i++ {
go m.Insert(i, i, &wg)
}
}()
go func() {
defer wg.Done()
for i := 0; i < work; i++ {
if i%4 == 0 {
wg.Add(1)
go m.Delete(i, &wg)
}
}
}()
go func() {
defer wg.Done()
for i := 0; i < work; i++ {
go m.Get(i, &wg)
}
}()
wg.Wait()
}
func operateSyncMap(sm *SMap, work int) {
wg := sync.WaitGroup{}
wg.Add(work*2 + 3)
go func() {
defer wg.Done()
for i := 0; i < work; i++ {
go sm.Insert(i, i, &wg)
}
}()
go func() {
defer wg.Done()
for i := 0; i < work; i++ {
if i%4 == 0 {
wg.Add(1)
go sm.Delete(i, &wg)
}
}
}()
go func() {
defer wg.Done()
for i := 0; i < work; i++ {
go sm.Get(i, &wg)
}
}()
wg.Wait()
}
func BenchmarkOperateSyncMap8Work(b *testing.B) {
sm := SMap{
sm: sync.Map{},
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
operateSyncMap(&sm, 8)
}
})
}
func BenchmarkOperateMap8Work(b *testing.B) {
m := Map{
m: make(map[int]int, 0),
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
operateMap(&m, 8)
}
})
}
func BenchmarkOperateSyncMap256Work(b *testing.B) {
sm := SMap{
sm: sync.Map{},
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
operateSyncMap(&sm, 256)
}
})
}
func BenchmarkOperateMap256Work(b *testing.B) {
m := Map{
m: make(map[int]int, 0),
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
operateMap(&m, 256)
}
})
}
执行go test -v -bench=. -benchmem的结果:
BenchmarkOperateSyncMap8Work-4 500000 3297 ns/op 256 B/op 23 allocs/op
BenchmarkOperateMap8Work-4 300000 7122 ns/op 16 B/op 1 allocs/op
BenchmarkOperateSyncMap256Work-4 10000 118631 ns/op 8227 B/op 767 allocs/op
BenchmarkOperateMap256Work-4 5000 301040 ns/op 67 B/op 1 allocs/op
写在最后,如果你知道为什么我测试的结果是这样的,请一定告诉我,毕竟我是个强迫症晚期患者,不知道原因的感觉很难受!!!
有疑问加站长微信联系(非本文作者)