从入门到掉坑:Go 内存池/对象池技术介绍(含GroupCache详解)( 二 )
代码中一共测试了 5 种情况 , 写入5000w的 keys 后 , 主动触发 2 次 GC 来测量耗时:
[1] With map[int32]*int32, GC took 456.159324ms[2] With map[int32]int32, GC took 10.644116ms[3] With map shards ([]map[int32]*int32), GC took 383.296446ms[4] With map shards ([]map[int32]int32), GC took 1.023655ms[5] With a plain slice ([]main.t), GC took 172.776μs
可以看到 , 当 map 中没有指针时 , 扫描停顿时间大约在 10ms 左右 , 而包含指针int32时则会扩大 45 倍 。
先看 5 的数据 , 单纯的 slice 速度飞快 , 基本没有 GC 消耗 。 而 map shards 就有点耐人寻味了 , 为什么我们没有对 map 加锁 , 分 shard 后 GC 时间还是缩短了呢?说好的将锁分布式化 , 才能提高性能呢?
坑 3: shards map 能提高性能的元凶(原因)要了解 shards map 性能变化的原因 , 需要先弄清楚 Golang GC 的机制 。 我们先加上GODEBUG=gctrace=1观察下 map 里包含指针与没有指针的 gc 差异:
map[]*int: gc 11 @11.688s 2%: 0.004+436+0.004 ms clock, 0.055+0/1306/3899+0.049 ms cpu, 1762->1762->1220 MB, 3195 MB goal, 12 P (forced)map[]int: gc 10 @9.357s 0%: 0.003+14+0.004 ms clock, 0.046+0/14/13+0.054 ms cpu, 1183->1183->746 MB, 2147 MB goal, 12 P (forced)
输出各字段含义可以看GODEBUG 之 gctrace 干货解析 , 这里我们只关注 cpu 里0.055+0/1306/3899+0.049 ms cpu 这段的解释:
- Mark Prepare (STW) - 0.055 表示整个进程在 mark 阶段 STW 停顿时间
- Marking - 0/1306/3899 三段信息 , 其中 0 是 mutator assist 占用时间 , 1306 是 dedicated mark workers+fractional mark worker 占用的时间 , 3899 是 idle mark workers 占用的时间(虽然被拆分为 3 种不同的 gc worker , 过程中被扫描的 P 还是会暂停的 , 另外注意这里时间是所有 P 消耗时间的总和)
- Mark Termination (STW) - 0.049 表示整个进程在 markTermination 阶段 STW 停顿时间
那回到上面的问题了 , shards map 的性能又是如何得到提升(近 10 倍)的?
// With map[int32]int32, GC took 11.285541msgc 1 @0.001s 7%: 0.010+2.1+0.012 ms clock, 0.12+0.99/2.1/1.2+0.15 ms cpu, 4->6->6 MB, 5 MB goal, 12 P...gc 8 @2.374s 0%: 0.003+3.9+0.018 ms clock, 0.042+0.31/6.7/3.1+0.21 ms cpu, 649->649->537 MB, 650 MB goal, 12 Pgc 9 @4.834s 0%: 0.003+7.5+0.021 ms clock, 0.040+0/14/5.1+0.25 ms cpu, 1298->1298->1073 MB, 1299 MB goal, 12 Pgc 10 @9.188s 0%: 0.003+26+0.004 ms clock, 0.045+0/26/0.35+0.053 ms cpu, 1183->1183->746 MB, 2147 MB goal, 12 P (forced)gc 11 @9.221s 0%: 0.018+9.4+0.003 ms clock, 0.22+0/17/5.0+0.043 ms cpu, 746->746->746 MB, 1492 MB goal, 12 P (forced)// With map shards ([]map[int32]int32), GC took 1.017494msgc 1 @0.001s 7%: 0.010+2.9+0.048 ms clock, 0.12+0.26/3.6/4.1+0.57 ms cpu, 4->7->6 MB, 5 MB goal, 12 P...gc 12 @3.924s 0%: 0.003+3.2+0.004 ms clock, 0.040+1.2/7.5/14+0.048 ms cpu, 822->827->658 MB, 840 MB goal, 12 Pgc 13 @8.096s 0%: 0.003+6.1+0.004 ms clock, 0.044+6.0/14/32+0.053 ms cpu, 1290->1290->945 MB, 1317 MB goal, 12 Pgc 14 @11.619s 0%: 0.003+1.2+0.004 ms clock, 0.045+0/2.5/3.7+0.056 ms cpu, 1684->1684->1064 MB, 1891 MB goal, 12 P (forced)gc 15 @11.628s 0%: 0.003+0.91+0.004 ms clock, 0.038+0/2.3/3.6+0.057 ms cpu, 1064->1064->1064 MB, 2128 MB goal, 12 P (forced)
从倒数第三轮内存最大的时候看 , GC worker 的耗时都是接近的;唯一差异较大的 , 是 markTermination 阶段的 STW 时间 , shard 方式下少了 1/10 , 因此推测和该阶段得到优化有关 。至于这个时间为什么能减少 , 我也不清楚为什么(这个坑挖得太深 , 只能以后找到资料再来填...)
2. GroupCache言归正传(众人:什么?!前面写这么多你还没进入正文 。 我:咳..咳..) , 我们总结下用 map 实现内存池的要点:
内存池用 map 不用 sync.Map;map 要加读写锁
map 尽量存非指针(key 和 value 都不包含指针)
map 里存放指针 , 需要注意 keys 过多会带来的 GC 停顿问题
使用 shards map
然后我们看看GroupCache 的实现方法 , 这个定义在 lru/lru.go 里:
// Cache is an LRU cache. It is not safe for concurrent access.type Cache struct {cache map[interface{}]*list.Element}
从 cache 的定义可以看出 , 这是我们说的 map 里包含指针的情况 , 而且还是不分 shards 的 。 所以如果你单机 GroupCache 里 keys 过多 , 还是要注意下用法的 。注:截止目前 1.14 , map 里包含指针时 idle worker 耗时问题还未有结论 , 有兴趣可以参考10ms-26ms latency from GC in go1.14rc1, possibly due to 'GC (idle)' work 里面的例子和现象 。
- 王文鉴|从工人到千亿掌门人,征服华为三星,只因他36年只坚持做一件事
- 手机|这个超强App,让手机快3倍,流畅到起飞
- 巅峰|realme巅峰之作:120Hz+陶瓷机身+5000mAh 做到了颜值与性能并存
- 现货供应|卢伟冰说到做到!120Hz+一亿像素,狂销30万首销现货供应
- 精英|业务流程图怎么绘制?销售精英的经验之谈
- 打响|拼多多打响双12首枪,iPhone12降到“mini价”,苹果11再见
- 深度|iPhone12到底值得买吗 深度体验一周我发现了这些
- 不到|苹果赚了多少?iPhone12成本不到2500元,华为和小米的利润呢?
- 走向|电商,从货架陈列走向内容驱动
- 星期一|亚马逊:黑五与网络星期一期间 第三方卖家销售额达到48亿美元