一些关于Golang GC的性能结论

155 阅读2分钟

前段时间在优化一个陈年Go程序的性能,通过pprof发现它在GC上消耗的性能非常多,本次优化的重点自然放在GC上。 为了优化得彻底一些,我做了一些实验来验证自己以前对GC在性能方面的认知。

本文不会讨论太多GC原理,但一个前置知识是,GC大致分为三步:1. 标记前的准备, 2. 遍历并标记, 3. 清理。 其中,时间主要消耗在第二步,也就是遍历所有对象并进行标记的流程上。这一步里,Golang GC要做的工作是遍历所有对象,并判断这个对象是否可以被回收。这里会涉及到三色标记法、写屏障等技术,不一一细说。但我们可以猜想到的一点是,GC时需要遍历的对象越多,性能越差。

本文也不会讨论太多关于性能优化的技巧(请参考我的其它文章),仅是对实验结果的一些记录。

实验方法

package main

import (
	"runtime"
	"runtime/debug"
)

func main() {
	debug.SetGCPercent(-1)
	runtime.GC()
	// code
	runtime.GC()
}

再使用 GODEBUG=gctrace=1 GOGC=off go run ./main.go​执行程序,看最后一次GC的输出结果。

一些结论

  • []struct​ 和[]*struct​ 相比, 无论是在内存占用还是GC上,都略胜一筹。内存占用更小是因为少用了一个指针。
  • map[xxx]struct​ 和map[xxx]*struct​相比,前者占更多内存,但在GC上性能更好。 (map[xxx]*struct​也是出了名的GC大户,尤其是当这个struct里字段多,嵌套深的时候)
  • 一个嵌套深的*struct​对GC的损耗很大。如果能把嵌套铺平也能明显提升性能
  • 经典的使用map[xxx]int​+[]*struct​ 替代 map[xxx]*struct​的方法非常有效。GC性能极大提升,内存减少并没有明显增大(甚至更少),访问时间也仅是极略微增加
  • chan 产生的GC负担与 slice 几乎一样。高频创建chan/slice还是会产生性能问题,一个经典的例子是hystrix对一次请求的处理中会有多个chan。
  • 使用*struct​实现的链表在GC上比[]*struct​稍差一些。但对链表来说更重要的是各项操作的性能,如果仅为了优化GC而使用slice实现链表,可能捡芝麻丢西瓜
  • sync.Pool​ 减轻的不是GC这个操作本身的压力,而是减少了重复新建对象的次数
  • slice/map的实际尺寸与GC压力成正比