Go 中怎么实现内存池,直接用 map 可以吗?常用库里 GroupCache、BigCache 的内存池又是怎么实现的?有没有坑?对象池又是什么?想看重点的同学,可以直接看 第 2 节 GroupCache 总结 。
0. 前言: tcmalloc 与 Go
以前 C++服务上线,遇到性能优化一定会涉及 Google 大名鼎鼎的 tcmalloc。
相比 glibc,tcmalloc 在多线程下有巨大的优势:
其中使用的就是内存池技术。如果想了解 tcmalloc 的细节,盗一张 图解 TCMalloc 中比较经典的结构图:
作为 Google 的得意之作, Golang 自然也用上了 tcmalloc 的 内存池 03 技术。因此我们普通使用 Golang 时, 无需关注内存分配的性能问题 。
1. 关于 map 你需要了解的
既然 Go 本身内存已经做了 tcmalloc 的管理,那实现缓存我们能想到的就是 map 了,是吧?(但仔细想想,map 不需要加锁吗?不加锁用 sync.Map 更好吗)
坑 1: 为什么不用 sync.Map
用过 map 的同学应该会知道,map 并不是线程安全的。多个协程同步更新 map 时,会有概率导致程序 core 掉。
那我们为什么不用 sync.Map ?当然不是因为 go 版本太老不支持这种肤浅原因。
坑 2: 用 map 做内存池就可以了?
并不能。map 存储 keys 也是有限制的,当 map 中 keys 数量超过 千万级 ,有可能造成性能瓶颈。
这个是我在之前业务中实际遇到的情况,当时服务里用了 GroupCache 做缓存,导致部分线上请求会超时(0.08%左右的超时率)。我们先暂时放下这个问题,弄清原因再来介绍这里的差异。
好消息是 2015 年 Go 开发者已经对 map 中无指针的情况进行了优化:
GC ignore maps with no pointers
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