添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Auto Byte

专注未来出行及智能汽车科技

微信扫一扫获取更多资讯

Science AI

关注人工智能与其他前沿技术、基础学科的交叉研究与融合发展

微信扫一扫获取更多资讯

deryzhou,腾讯 PCG 后台开发工程师 作者

从入门到掉坑:Go 内存池/对象池技术介绍

Go 中怎么实现内存池,直接用 map 可以吗?常用库里 GroupCache、BigCache 的内存池又是怎么实现的?有没有坑?对象池又是什么?想看重点的同学,可以直接看 第 2 节 GroupCache 总结

0. 前言: tcmalloc 与 Go

以前 C++服务上线,遇到性能优化一定会涉及 Google 大名鼎鼎的 tcmalloc。

相比 glibc,tcmalloc 在多线程下有巨大的优势:

vs tcmalloc

其中使用的就是内存池技术。如果想了解 tcmalloc 的细节,盗一张 图解 TCMalloc 中比较经典的结构图:

图解 TCMalloc

作为 Google 的得意之作, Golang 自然也用上了 tcmalloc 的 内存池 03 技术。因此我们普通使用 Golang 时, 无需关注内存分配的性能问题

1. 关于 map 你需要了解的

既然 Go 本身内存已经做了 tcmalloc 的管理,那实现缓存我们能想到的就是 map 了,是吧?(但仔细想想,map 不需要加锁吗?不加锁用 sync.Map 更好吗)

坑 1: 为什么不用 sync.Map

2020-05-09 补充:多位同学也提到了,bigcache 这个测试并不公平。查了下 issues,map+lock 和 sync.Map 的有人做过测试,性能确实低一些(单锁的情况)https://github.com/golang/go/issues/28938#issuecomment-441737879

但如果是 shards map+lock 和 sync.Map,在不同的读写比(比如读多写少,当超时才更新)时,这块就不好判断哪种实现更优了,有兴趣的同学可以尝试深挖下(而且 doyenli 也提到,sync.Map 内部是 append only 的)

用过 map 的同学应该会知道,map 并不是线程安全的。多个协程同步更新 map 时,会有概率导致程序 core 掉。

那我们为什么不用 sync.Map ?当然不是因为 go 版本太老不支持这种肤浅原因。

https://github.com/allegro/bigcache-bench 里有张对比数据,纯写 map 是比 sync.Map 要快很多,读也有一定优势。考虑到多数场景下读多写少,我们只需对 map 加个 读写锁 ,异步写的问题就搞定了(还不损失太多性能)。

map vs sync.Map

除了读写锁,我们还可以使用 shard map 的分布式锁来继续提高并发(后面 bigcache 部分会介绍),所以你看最终的 cache 库里,大家都没用 sync.Map,而是用 map+读写锁 来实现存储。

坑 2: 用 map 做内存池就可以了?

并不能。map 存储 keys 也是有限制的,当 map 中 keys 数量超过 千万级 ,有可能造成性能瓶颈。

这个是我在之前业务中实际遇到的情况,当时服务里用了 GroupCache 做缓存,导致部分线上请求会超时(0.08%左右的超时率)。我们先暂时放下这个问题,弄清原因再来介绍这里的差异。

找了下资料,发现 2014 年 Go 有个 issue 提到 Large maps cause significant GC pauses 的问题。简单来说就是当 map 中存在大量 keys 时, GC 扫描 map 产生的停顿将不能忽略

好消息是 2015 年 Go 开发者已经对 map 中无指针的情况进行了优化:

GC ignore maps with no pointers

我们参考其中的代码,写个 GC 测试程序 验证下:

package main 
import (   "fmt"   "os"   "runtime"   "time" )
 // Results of this program on my machine: // // for t in 1 2 3 4 5; do go run maps.go $t; done // // Higher parallelism does help, to some extent: // // for t in 1 2 3 4 5; do GOMAXPROCS=8 go run maps.go $t; done // // Output(go 1.14): // With map[int32]*int32, GC took 456.159324ms // With map[int32]int32, GC took 10.644116ms // With map shards ([]map[int32]*int32), GC took 383.296446ms // With map shards ([]map[int32]int32), GC took 1.023655ms // With a plain slice ([]main.t), GC took 172.776µs 
func main() {   const N = 5e7 // 5000w