笔者使用 map 作为一个全局的 cache,测试之后发现,即使删除了不需要使用 key,但随着写入数据量的增加,map 占用的内存也开始不断增加。
分析原因,map 是通过 key 和 hash 值来分布和查找对象。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
性能测试: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
内存分配测试: 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 第二次初始化
在对标准库做额外的审查和性能分析之后,Go 团队成员发现当使用 sync.RWMutex 的代码被部署在「很多核 」的 CPU 上高并发读的场景下,它的性能远低于理想值。所以使用 sync.RWMutex 封装的数据结构中读取数据的性能会受很大影响。
注意: 在2017 GopherCon有一个闪电演讲:An overview of sync.Map介绍了关于sync.Map
诞生的原因以及它的设计目标。如果你在考虑使用这个实现,建议你一定要看下这个视频,视屏中讲解了它可能有的一些性能陷阱。
sync.Map 类型是针对两种常见用例进行优化的:
笔者翻译水平有限,原文如下:
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 的使用方式——多个 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
写在最后,如果你知道为什么我测试的结果是这样的,请一定告诉我,毕竟我是个强迫症晚期患者,不知道原因的感觉很难受!!!