- 哈希值应该比较随机 (质量)
- 哈希速度比较快 (速度)
- 尽量不产生额外的内存分配 , 避免对垃圾回收产生压力 (耗费资源少)
bigcache 提供了一个默认的 Hash 的实现 , 采用 fnv64a 算法 。这个算法的好处是采用位运算的方式在栈上进行运算 , 避免在堆上分配 。
type fnv64a struct{}const ( // offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash offset64 = 14695981039346656037 // prime64 FNVa prime value. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash prime64 = 1099511628211)// Sum64 gets the string and returns its uint64 hash value.func (f fnv64a) Sum64(key string) uint64 { var hash uint64 = offset64 for i := 0; i < len(key); i++ {hash ^= uint64(key[i])hash *= prime64 } return hash}忽略内存开销对于 Go 语言中的 map, 垃圾回收器在 mark 和 scan 阶段检查 map 中的每一个元素, 如果缓存中包含数百万的缓存对象 , 垃圾回收器对这些对象的无意义的检查导致不必要的时间开销 。
bigcache 的作者做了测试 。他们测试了简单的 HTTP/JSON 序列化(不会访问 cache) 。在 cache 为空的时候 1 万的 QPS 的耗时大约 10 毫秒 。当 cache 填满的时候 , 99% 的请求都会超过 1 秒 。监控显示堆中包含 4 千万的对象 , GC 过程中的 mark 和 scan 也需要 4 秒 。
我们可以很容易测试这种状况 , 比如下面的代码:
package mainimport "time"type Item struct { A string B string C string D string E string F string G G}type G struct { H int I int K int L int M int N int}func main() { m := make(map[int]*Item, 10*1024*1024) for i := 0; i < 1024*1024; i++ {m[i] = &Item{} } for i := 0; ; i++ {delete(m, i)m[1024*1024+i] = &Item{}time.Sleep(10 * time.Millisecond) }}只有一个 map 对象 , 里面包含一百万的元素 , 每 10 毫秒删一个放一个 。
并发量相当小 , 并且单个的 goroutine 也没有竞争 , 但是由于元素的数量巨大 , 垃圾回收在mark/scan阶段需要花费上百毫秒进行标记和遍历 。
文章插图
那么如何解决这个问题呢?
我们知道垃圾回收器检查的是堆上的资源 , 如果不把对象放在堆上 , 不就解决这个问题了吗?还真有这样的项目offheap[11] , 它提供了定制的Malloc() 和 Free() , 但是你的缓存需要基于这些方法定制 。当然一些基于垃圾回收的编程语言为了减少垃圾回收的时间 , 都会提供相应的库 , 比如Java: ChronicleMap, Part 1: Go Off-Heap[12] 。堆外内存很容易产生内存泄漏 。
第二种方式是使用 freecache[13] 。freecache 通过减少指针的数量以零 GC 开销实现 map 。它将键和值保存在ringbuffer中 , 并使用索引查找对象 。
第三种优化方法是和 Go 1.5 中一个修复有关(#9477[14]), 这个 issue 还描述了包含大量对象的 map 的垃圾回收时的耗时问题 , Go 的开发者优化了垃圾回收时对于 map 的处理 , 如果 map 对象中的 key 和 value 不包含指针 , 那么垃圾回收器就会对它们进行优化:
runtime: do not scan maps when k/v do not contain pointers所以如果我们的对象不包含指针 , 虽然也是分配在堆上 , 但是垃圾回收可以无视它们 。
Currently we scan maps even if k/v does not contain pointers. This is required because overflow buckets are hanging off the main table. This change introduces a separate array that contains pointers to all overflow buckets and keeps them alive. Buckets themselves are marked as containing no pointers and are not scanned by GC (if k/v does not contain pointers).
This brings maps in line with slices and chans -- GC does not scan their contents if elements do not contain pointers.
Currently scanning of a map[int]int with 2e8 entries (~8GB heap) takes ~8 seconds. With this change scanning takes negligible time.
https://go-review.googlesource.com/c/go/+/3288
如果我们把 map 定义成 map[int]int , 就会发现 gc 的耗时就会降下来了 。
文章插图
遗憾的是 , 我们没办法要求用户的缓存对象只能包含int、bool这样的基本数据类型 。
解决办法就是使用哈希值作为map[int]int的 key 。把缓存对象序列化后放到一个预先分配的大的字节数组中 , 然后将它在数组中的 offset 作为map[int]int的 value 。
推荐阅读
- 手把手教你分析具体链接的谷歌曝光量和点击率
- 养颜美容 几款科学饮茶方推荐
- 职场霸凌|网络诽谤、职场霸凌……最高法院法官为你答疑
- 螃蟹要怎样蒸应该怎样放在锅里 螃蟹放盘子里蒸还是直接放篦子上蒸
- 怎么对付瞧不起那些亲戚的人 怎么对付欺负你的亲戚长辈
- 美甲|美甲工具和各种必用品
- 你喂员工吃草,却指望他们有狼性?
- 想要加薪却张不开嘴?学会“福格行为模型”,加薪真没那么难
- 买芝士时,选“再制干酪”还是“奶酪”?有啥区别?学会别挑错了
- 温柔的穿搭也太招人喜欢了!学会这么搭配,优雅高级