本文中我们将分享几个非常有用的优化技巧用以改善许多常见的 GC 性能问题(接下来还将覆盖一些有趣的死锁问题)。我们将重点分享如何通过嵌套结构体、使用 sync.Pool、和复用后端数组减少内存分配和降低 GC 开销。
一、减少内存分配和 GC 优化
将 Go 与其他语言(比如 java )区别开来的是 Go 语言能让你管理内存布局。通过 GO 语言,你可以合并碎片,而其他垃圾集合语言不能。
让我们看看 CockroachDB 中从磁盘读取数据并解码的一小段代码:
为了读取数据,我们执行了 4 次内存分配:MVCCMetadata 结构体、MVCCValue 结构体和 metaKey、valueKey。在 Go 语言中我们可以通过合并结构体和预分配空间给 Key 把内存分配减少为 1 次。
我们声明了一个 getBuffer 类型,包含两个不同的结构体:MVCCMetadata 和 MVCCValue (都是 protobuf 对象),不同于通常使用的切片,第三个成员使用了一个数组。
不需要额外分配内存,你就可以直接在结构体中定义一个定长的数组( 1024 bytes ),这允许我们将三个对象放到同一个 getBuffer 结构体中。这样我们就把 4 次内存分配减少为 1 次。需要注意的的两个不同的 key 我们使用了同一个数组,在两个 key 不同时使用的情况下是可以正常工作的。稍后我们再来讨论数组。
二、sync.Pool
说实话,我们花了一段时间才弄明白为什么 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 的情况可能会有但是不常见,在这种情况下,程序可以透明的自动分配新的后端数组,我们的代码不需要做任何处理。
1、Gogoprotobuf vs Google protobuf
最后,我们在磁盘上存储所有的数据都使用了 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, 龙猫,柚子