几周前咱们分享了一个帖子讲述咱们为何选择Go语言编写CockroachDB,咱们收到一些问题,询问咱们是如何解决Go语言的一些已知问题,特别是关于性能、GC和死锁的问题。java
本文中咱们将分享几个很是有用的优化技巧用以改善许多常见的GC性能问题(接下来还将覆盖一些有趣的死锁问题)。咱们将重点分享如何经过嵌套结构体、使用 sync.Pool、和复用后端数组减小内存分配和下降GC开销。后端
将Go与其余语言(好比java)区别开来的是Go语言能让你管理内存布局。经过GO语言,你能够合并碎片,而其余垃圾集合语言不能。数组
让咱们看看CockroachDB中从磁盘读取数据并解码的一小段代码:性能优化
metaKey := mvccEncodeMetaKey(key) var meta MVCCMetadata if err := db.GetProto(metaKey, &meta); err != nil { // Handle err } ... valueKey := makeEncodeValueKey(meta) var value MVCCValue if err := db.GetProto(valueKey, &value); err != nil { // Handle err }
为了读取数据,咱们执行了4次内存分配:MVCCMetadata结构体、MVCCValue结构体和metaKey、valueKey。在Go语言中咱们能够经过合并结构体和预分配空间给Key把内存分配减小为1次。mvc
type getBuffer struct { meta MVCCMetadata value MVCCValue key [1024]byte } var buf getBuffer metaKey := mvccEncodeKey(buf.key[:0], key) if err := db.GetProto(metaKey, &buf.meta); err != nil { // Handle err } ... valueKey := makeEncodeValueKey(buf.key[:0], meta) if err := db.GetProto(valueKey, &buf.value); err != nil { // Handle err }
咱们声明了一个getBuffer类型,包含两个不一样的结构体:MVCCMetadata和MVCCValue(都是protobuf对象),不一样于一般使用的切片,第三个成员使用了一个数组。app
不须要额外分配内存,你就能够直接在结构体中定义一个定长的数组(1024 bytes),这容许咱们将三个对象放到同一个getBuffer结构体中。这样咱们就把4次内存分配减小为1次。须要注意的的两个不一样的key咱们使用了同一个数组,在两个key不一样时使用的状况下是能够正常工做的。稍后咱们再来讨论数组。函数
var getBufferPool = sync.Pool{ New: func () interface{} { return &getBuffer{} }, }
说实话,咱们花了一段时间才弄明白为何 sync.Pool 才是咱们咱们想要的。在一个GC周期内能够无限制使用同一个对象无需屡次内存分配,GC会负责回收。在每次GC启动的时候都会清除Pool中的对象。布局
用一个例子来讲明如何使用 sync.Pool:性能
buf := getBufferPool.Get().(*getBuffer) defer getBufferPoolPut(buf) key := append(but.key[0:0], ...)
首先你须要使用一个工厂函数来声明一个全局的 sync.Pool 对象,在这个列子中咱们分配一个 getBuffer结构体并返回。咱们再也不建立新的 getBuffer 改成从 pool 中获取。Pool.Get 返回的是一个空接口,咱们须要使用类型断言转换。使用完成后再放回到 pool 中。最终的结果是咱们无需每次获取 getBuffer时都分配一次内存。测试
有些事可能不值一提,在Go语言中数组和切片是不一样的类型,并且切片和数组几乎全部操做都同样。你仅仅经过一个方括号语法 [:0] 就能够从数组获得一个切片。
key := append(bf.key[0:0], ...)
这里使用数组建立了一个长度为0的切片。事实是这个切片已经拥有了一个后端存储,意思是说对切片的append操做实际上插入到数组中,而并无分配新的内存。因此当咱们解码一个key时,咱们能够append进一个经过这个 buffer 建立的切片中。只要key的长度小于 1 KB,咱们就不须要作任何内存分配。将复用咱们给数组分配的内存。
key 的长度超过 1 KB 的状况可能会有可是不常见,在这种状况下,程序能够透明的自动分配新的后端数组,咱们的代码不须要作任何处理。
最后,咱们在磁盘上存储全部的数据都使用了protobuf。然而咱们并无使用 Google官方的protobuf类库,咱们强烈推荐使用一个叫作 gogoprotobuf的分支。
Gogoprotobuf 遵循了不少咱们上面提到的关于避免没必要要的内存分配的原则。尤为是,它容许将数据编码到一个后端使用数组的字节切片以免屡次内存分配。此外,非空注解容许你直接嵌入消息而无需额外的内存分配开销,这在始终须要嵌入消息时是很是有用的。
最后一点优化是,较基于反射进行编码和解编码的Google标准protobuf类库,gogoprotobuf使用编码和解编码协程提供了不错的性能改善。
经过结合上述技巧,咱们已经能够最小化GC的性能开销和优化更好的性能。当咱们接近测试阶段,更多地专一于内存分析,咱们将在后续的帖子中分享咱们的成果。固然,若是你知道其余的Go语言性能优化,咱们洗耳恭听。
原文连接:http://www.cockroachlabs.com/blog/how-to-optimize-garbage-collection-in-go/原文做者:Jessica Edwards翻译校对:betty, 龙猫,柚子