本文记录了ecachev1.0.5到v1.1.0的性能优化过程
背景介绍
准备工作
原则
- 基于真实的度量。——《重构——改善现有代码的设计》P69
哪怕你完全了解系统,也请实际度量它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。
思路
工具
- golang pprof
- graphivs(用来生成剖析结果图片)
- mac下安装命令:
brew install graphviz
步骤
sh>GO111MOUDLE=off go test -bench=BenchmarkGetInt_ecache ecache_test.go -cpuprofile=cpu.prof
sh>go tool pprof benchplus.test cpu.prof
交互模式下:(pprof) svg
优化一:读取性能(从100ns/op到40ns/op)
- 总体还是比较符合预期的,毕竟在性能方面已经有所考量和侧重,但是在最初的测试中,优势依然不是特别明显,比如读取性能,最快的
bigcache读取整型值的性能在 80ns/op 左右,但是ecache在第一版只能跑出 100ns/op 左右的性能。
hashCode占了总耗时的50%
![pic1_hashCode.png]()
func hashBKRD(s string) (hash int32) {
for i := 0; i < len(s); i++ {
hash = hash*131 + int32(s[i])
}
return hash
}
继续剖析——time.Now()占了总耗时的33%
![pic2_time.Now.png]()
-
优化思路:由于内部只需要时间戳,并且缓存系统要求的时间戳并不一定那么精准,所以考虑用维护一个全局时间戳的方式来优化———短期自增(每100ms)、定期校准(约1s)
-
time.Now()【代码版本快照】改为内部计时器【commit-8dc1fa7d】,获取当前时间使用内部的now()方法可直接获得时间戳,而不再需要使用会产生临时对象的time.Now().UnixNano()
-
内部计时器最初采用time.Timer实现,实际测试发现定时器会受系统压力影响,精度无法保证,后改为time.Sleep【commit-92245e4b】
var clock = time.Now().UnixNano()
func now() int64 { return atomic.LoadInt64(&clock) }
func init() {
go func() {
for {
atomic.StoreInt64(&clock, time.Now().UnixNano()) // 每秒校准
for i := 0; i < 9; i++ {
time.Sleep(100 * time.Millisecond)
atomic.AddInt64(&clock, int64(100*time.Millisecond))
}
time.Sleep(100 * time.Millisecond)
}
}()
}
- 本次优化完成以后,读取整型性能提升至40ns/op,从设计的指标来看,
ecache的数据都已名列前茅![pic3_bench.png]()
优化二:GC耗时(从3倍耗时到超越)
针对双链表的改进思路
-
双链表节点实现成不需要产生临时节点指针的形式
type node struct {
k string
v value
expireAt int64 // 纳秒时间戳,为0说明被标记删除
}
type cache struct {
dlnk [][2]uint16 // 双链表索引列表,第0个元素存储{尾节点索引,头节点索引},其他元素存{前序节点索引,后继节点索引}
m []node // 预分配连续空间内存
hmap map[string]uint16 // <key,dlnk中的位置>
last uint16 // 没有满时,分配到的位置
}
-
一些取巧的设计
- 只用一个
last字段和连续节点空间的容量比较来判断是否分配满if c.last == uint16(cap(c.m)) // 分配满了
- 用uint16类型存储索引,节省空间的同时,配合桶的数量,足够大
dlnk用n+1个元素来存储索引,每个元素都是{前序节点索引, 后继节点索引}
- 索引为0代表空,刚好
dlnk[0]存储的是{尾节点索引,头节点索引}
- 因为头尾节点和其他节点存储在一起,复用
adjust方法,通过参数就能实现将元素移动到头部还是尾部的功能
ajust(x, p, n)移动到头部
ajust(x, n, p)移动到尾部
- 删除元素时复用时间戳,设置为0代表删除,并且移动到链表尾部
-
调整完效果还不错,mallogc缩短了、_refresh时候的gcWriteBarrier也不见了![pic4_gcWriteBarrier.png]()
进一步优化
type value struct {
v *interface{} // 存放任意类型
i int64 // 存放整型
}
还差最后一步
type value struct {
i *interface{} // 存放任意类型
b []byte // 存放字节数组
}
其他改进
- 时间戳原来记录的是写入时间,群友review提出了时间回跳可能会有问题,改为
expireAt过期的时间点,保证一定会在设置的过期时间内过期
- 仔细检查并发场景下
node复用可能导致取到错误值的情况
优化结果
![pic6_bench.png]()
🐌 代表很慢,✈️ 代表快,🚀 代表非常快,可以看到优化以后的ecache,各项测试表现都不错(除大量并发写入整型的GC耗时无法超过bigcache外)。
|
bigcache |
cachego |
ecache |
freecache |
gcache |
gocache |
| PutInt |
✈️ |
|
🚀 |
🚀 |
✈️ |
✈️ |
| GetInt |
✈️ |
✈️ |
🚀 |
|
✈️ |
✈️ |
| Put1K |
✈️ |
✈️ |
🚀 |
🚀 |
🚀 |
✈️ |
| Put1M |
🐌 |
|
🚀 |
🐌 |
✈️ |
✈️ |
| PutTinyObject |
✈️ |
|
🚀 |
🚀 |
✈️ |
|
| ChangeOutAllInt |
✈️ |
|
🚀 |
🚀 |
✈️ |
✈️ |
| HeavyReadInt |
🚀 |
🚀 |
🚀 |
|
🚀 |
|
| HeavyReadIntGC |
✈️ |
🚀 |
🚀 |
|
✈️ |
✈️ |
| HeavyWriteInt |
🚀 |
✈️ |
🚀 |
🚀 |
|
✈️ |
| HeavyWriteIntGC |
🚀 |
|
✈️ |
✈️ |
|
|
| HeavyWrite1K |
🐌 |
✈️ |
🚀 |
🚀 |
|
✈️ |
| HeavyWrite1KGC |
🐌 |
✈️ |
🚀 |
🚀 |
|
✈️ |
| HeavyMixedInt |
🚀 |
✈️ |
🚀 |
|
✈️ |
🚀 |
版本对比
参考资料