前段时间在优化一个陈年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压力成正比