热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

golang下Map的使用和性能分析(勿滥用锁)

golang 中 map 性能优化[低阶]

简单介绍

golang 中的 build-in 的 map 这个 map 是非线程安全的,但是也是最常用的一个家伙。 为了测试多个 map 的性能我写了个接口 Map

type Map interface {
 Set(key string, val interface{})
 Get(key string) (interface{}, bool)
 Del(key string)
}

然后这是封装的普通的 map

type OriginMap struct {
 m map[string]interface{}
}

func NewOriginMap() *OriginMap {
 return &OriginMap{m: make(map[string]interface{})}
}

func (o *OriginMap) Get(key string) (interface{}, bool) {
 v, ok := o.m[key]
 return v, ok
}
func (o *OriginMap) Set(key string, value interface{}) {
 o.m[key] = value
}

func (o *OriginMap) Del(key string) {
 delete(o.m, key)
}

别看一堆代码,其实就是 get 和 set 操作。在这里我们要使用 golang 自带的 test 工具

func TestOriginMaps(t *testing.T) {
 hm := NewOriginMap()
 wg := sync.WaitGroup{}
 for i := 0; i 

这其中有个变量 Writer 就是写者的数量,如果只有 1 的时候程序能安全运行退出

1264 ± : go test map_test/map_performance_test.go -v           ⏎ [3h1m] ✹ ✚ ✭
=== RUN   TestOriginMaps
--- PASS: TestOriginMaps (0.00s)
    map_performance_test.go:71: Get 0 = 0
    map_performance_test.go:71: Get 1 = 1
......
    map_performance_test.go:71: Get 99 = 9801
PASS
ok      command-line-arguments  0.339s

但是一旦我们把 Writer 数量改为 2

1264 ± : go test map_test/map_performance_test.go -v             [3h2m] ✹ ✚ ✭
=== RUN   TestOriginMaps
fatal error: concurrent map writes

goroutine 21 [running]:

立马就爆炸了。那么????golang 自己官方心理没数么?

当然有数 golang 开发者其中之一可是拿图灵奖的。你可以点击*** 上的讨论[1]和github 这里[2]去查看相关的 issue

Sync. Map

这是某大佬提出的解决方案,我们试试

type SyncMap struct {
 m sync.Map
}

func NewSyncMap() *SyncMap {
 return &SyncMap{}
}

func (o *SyncMap) Get(key string) (interface{}, bool) {
 v, ok := o.m.Load(key)
 return v, ok
}
func (o *SyncMap) Set(key string, value interface{}) {
 o.m.Store(key, value)
}

func (o *SyncMap) Del(key string) {
 o.m.Delete(key)
}

我简单封装了一下,测试个性能没啥问题。

现在把 Write 增加也没问题了,可是真的没问题么?

我们现在小改一下第一种 map 加了个 RW 锁,然后和这种 map 做一下比较看看?

type OriginWithRWLock struct {
 m map[string]interface{}
 l sync.RWMutex
}

func NewOriginWithRWLock() *OriginWithRWLock {
 return &OriginWithRWLock{
  m: make(map[string]interface{}),
  l: sync.RWMutex{},
 }
}

func (o *OriginWithRWLock) Get(key string) (interface{}, bool) {
 o.l.RLock()
 v, ok := o.m[key]
 o.l.RUnlock()
 return v, ok
}
func (o *OriginWithRWLock) Set(key string, value interface{}) {
 o.l.Lock()
 o.m[key] = value
 o.l.Unlock()
}

func (o *OriginWithRWLock) Del(key string) {
 o.l.Lock()
 delete(o.m, key)
 o.l.Unlock()
}

然后我们这次用 Test 里的 Benchmark 试试看,为了方便比较,我们写一个函数 benchmarkMap。

func benchmarkMap(b *testing.B, hm Map) {
 var wg sync.WaitGroup
 for i := 0; i 

首先是 BenchMark 的函数当使用

go test .... -bench=. -benchmem

的时候会被调用,然后来测试两种 Map 性能,上面那个是测试性能的函数,分别对两个函数的进行测试~~拭目以待

当两者都是 100 的时候
 go test test_map/map_test.go  -v -bench=. -benchmem                                                                                                                         [14:12:59]
goos: darwin
goarch: amd64
BenchmarkMaps
    map_test.go:73: Writer: 100,Reader: 100
BenchmarkMaps/SyncMap
BenchmarkMaps/SyncMap-8                       80          13374265 ns/op         1710981 B/op      80867 allocs/op
BenchmarkMaps/map_with_RWLock
BenchmarkMaps/map_with_RWLock-8              100          12572631 ns/op          155019 B/op      16951 allocs/op
PASS
ok      command-line-arguments  3.323s

基本上 SyncMap 的整体性能是优于 mapWithRWLock 的我来分析一下为什么

从古至今,人们一直在时间和空间上做斗争,这次也不例外,两种锁的实现原理不一样。

golang下Map的使用和性能分析(勿滥用锁)

图1:带锁的 map

当我们使用普通 Map 带 RWMutex 会将整块内存锁住,然后其他请求就要等待。 SyncMap 是如何实现的呢?

golang下Map的使用和性能分析(勿滥用锁)

图2:SyncMap

它分为两块内存(存的都是指针),一块只读区域,一块 Dirty 区域支持读写。

两边的指针指向原数据,当需要 Get 的时候他会执行 Load 操作从 Read 中去获取指针指向的值,如果没有找到( miss )发生了,就转而会去 dirty 中获得数据并且存入 Read 中。

当 miss 超过一定数量的时候,他就会用原子操作把 dirty 的数据 Promote 到 ReadOnly 中。

因此 Sync 这种机制,往往只适用于 Key-Value 相对稳定的业务情况,读多写少的业务。

手痒想写个内存的看看到底多花多少内存 go tool pprof 是一个工具可以查看代码测评产生的内存日志
go test map_test/map_performance_test.go -bench=. -memprofile=mem.prof
go tool pprof map.test mem.prof
(pprof)top
...
      flat  flat%   sum%        cum   cum%
    1.54GB 57.95% 57.95%     1.57GB 59.14%  command-line-arguments_test.benchmarkMap.func2
    0.43GB 16.22% 74.17%     0.69GB 25.94%  command-line-arguments_test.benchmarkMap.func1

不用说了这看起来三倍的内存消耗,果然越快内存越大。那么?本次测评到此结束?

!!! 并没有!!! 还有一个大佬写了个 concurrent-map 甚叼,我们来观摩一波。concurrent-map[3]

立马封装一波

type ConCurrentMap struct {
 m cmap.ConcurrentMap
}

func NewConCurrentMap() *ConCurrentMap {
 conMap := cmap.New()
 return &ConCurrentMap{m: conMap}
}

func (c *ConCurrentMap) Get(key string) (interface{}, bool) {
 v, ok := c.m.Get(key)
 return v, ok
}
func (c *ConCurrentMap) Set(key string, value interface{}) {
 c.m.Set(key, value)
}

func (c *ConCurrentMap) Del(key string) {
 c.m.Remove(key)
}

迫不及待开始测试,当 Write=100,Reader=100 的时候

go test test_map/map_test.go  -v -bench=. -benchmem                                                                                                                         [14:24:48]
goos: darwin
goarch: amd64
BenchmarkMaps
    map_test.go:73: Writer: 1000,Reader: 1000
BenchmarkMaps/SyncMap
BenchmarkMaps/SyncMap-8                        8         129847762 ns/op        16410356 B/op     785167 allocs/op
BenchmarkMaps/map_with_RWLock
BenchmarkMaps/map_with_RWLock-8               10         117275854 ns/op         1723905 B/op     169971 allocs/op
BenchmarkMaps/CMap
BenchmarkMaps/CMap-8                          69          27681675 ns/op         1786702 B/op     169936 allocs/op
PASS
ok      command-line-arguments  4.424s

那么我同样做个表格吧,把读写的几种情况都列出来

R/W SyncMap map_with_RWLock CMap

最后说一下这个并发读map是怎么搞的

golang下Map的使用和性能分析(勿滥用锁)

图2:SyncMap

左边是普通的map,当有读写的时候锁上了,其他线程就无法读写了。右边的是 concurrentMap ,他利用了一种 partition 的思想,把 Map 的内存 SHARD (分割)成N份,然后用不同的 锁上锁,那么降低了需要资源被锁的概率。

我们在日常中编程的时候容易陷入一种误区,就是这锁,那锁,全锁上,面试也在问各种锁,但是在真实QPS比价高的业务中,锁是一种很可怕的东西,如果能在编程的时候好好想想写出 **LockFree`的程序是最好的啊。

我是北京某211的混子,从19年10月开始写两行golang到现在不知不觉已经过去了2个月,上手就开始拉框架写代码的我已经进化到开始分析性能,然后优化代码啦,如果有小伙伴想一起讨论讨论,欢迎。

参考资料

[1]

***这里: ions/11063473/map-with-concurrent-access

[2]

github 上的讨论: sues/21035

[3]

concurrent-map: urrent-map

编辑于 2020-12-31

推荐阅读
  • 1:有如下一段程序:packagea.b.c;publicclassTest{privatestaticinti0;publicintgetNext(){return ... [详细]
  • 本文详细探讨了Java中的24种设计模式及其应用,并介绍了七大面向对象设计原则。通过创建型、结构型和行为型模式的分类,帮助开发者更好地理解和应用这些模式,提升代码质量和可维护性。 ... [详细]
  • golang常用库:配置文件解析库/管理工具viper使用
    golang常用库:配置文件解析库管理工具-viper使用-一、viper简介viper配置管理解析库,是由大神SteveFrancia开发,他在google领导着golang的 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 技术分享:从动态网站提取站点密钥的解决方案
    本文探讨了如何从动态网站中提取站点密钥,特别是针对验证码(reCAPTCHA)的处理方法。通过结合Selenium和requests库,提供了详细的代码示例和优化建议。 ... [详细]
  • 本文详细介绍了如何在Linux系统上安装和配置Smokeping,以实现对网络链路质量的实时监控。通过详细的步骤和必要的依赖包安装,确保用户能够顺利完成部署并优化其网络性能监控。 ... [详细]
  • C++实现经典排序算法
    本文详细介绍了七种经典的排序算法及其性能分析。每种算法的平均、最坏和最好情况的时间复杂度、辅助空间需求以及稳定性都被列出,帮助读者全面了解这些排序方法的特点。 ... [详细]
  • 本文详细介绍了 Dockerfile 的编写方法及其在网络配置中的应用,涵盖基础指令、镜像构建与发布流程,并深入探讨了 Docker 的默认网络、容器互联及自定义网络的实现。 ... [详细]
  • 深入解析JVM垃圾收集器
    本文基于《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版,详细探讨了JVM中不同类型的垃圾收集器及其工作原理。通过介绍各种垃圾收集器的特性和应用场景,帮助读者更好地理解和优化JVM内存管理。 ... [详细]
  • 使用Numpy实现无外部库依赖的双线性插值图像缩放
    本文介绍如何仅使用Numpy库,通过双线性插值方法实现图像的高效缩放,避免了对OpenCV等图像处理库的依赖。文中详细解释了算法原理,并提供了完整的代码示例。 ... [详细]
  • Windows服务与数据库交互问题解析
    本文探讨了在Windows 10(64位)环境下开发的Windows服务,旨在定期向本地MS SQL Server (v.11)插入记录。尽管服务已成功安装并运行,但记录并未正确插入。我们将详细分析可能的原因及解决方案。 ... [详细]
  • PyCharm中配置Pylint静态代码分析工具
    本文详细介绍如何在PyCharm中配置和使用Pylint,帮助开发者进行静态代码检查,确保代码符合PEP8规范,提高代码质量。 ... [详细]
  • 本文介绍了如何使用 Spring Boot DevTools 实现应用程序在开发过程中自动重启。这一特性显著提高了开发效率,特别是在集成开发环境(IDE)中工作时,能够提供快速的反馈循环。默认情况下,DevTools 会监控类路径上的文件变化,并根据需要触发应用重启。 ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • c# – UWP:BrightnessOverride StartOverride逻辑 ... [详细]
author-avatar
sunhuan
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有