用golang开发系统软件的一些细节( 二 )


sync.Pool不如上面的方法节省内存,但好处是可以缩容 。
数量可控的中型对象有的时候,我们可能需要一些定额数量的对象,并且对这些对象复用 。
这时可以使用channel来做内存池 。需要时从channel取出,用完放回channel 。
slice的复用fasthttp, VictoriaMetrics等组件的作者 valyala可谓是把slice复用这个技巧玩上了天,具体可以看fasthttp主页上的Tricks with []byte buffers这部分介绍 。
概要的总结起来就是:[]byte这样的数组分配后,不要释放,然后下次使用前,用slice=slice[:0]来清空,继续使用其上次分配好的cap指向的空间 。
这篇中文的总结也非常不错:《fasthttp对性能的优化压榨》
valyala大神还写了个 bytebufferpool,对[]byte重用的场景进行了封装 。
避免容器空间动态增长对于slice和map而言,在预先可以预估其空间占用的情况下,通过指定大小来减少容器操作期间引起的空间动态增长 。特别是map,不但要拷贝数据,还要做rehash操作 。
func xxx(){  slice := make([]byte, 0, 1024)  // 有的时候,golangci-lint会提示未指定空间的情况  m := make(map[int64]struct{}, 1000)}大神技巧:用slice代替map此技巧源于valyala大神 。
假设有一个很小的map需要插入和查询,那么把所有key-value顺序追加到一个slice中,然后遍历查找——其性能损耗可能比分配map带来的GC消耗还要小 。

  1. map变成slice,少了很多动态调整的空间
  2. 如果整个slice能够塞进CPU cache line,则其遍历可能比从内存load更加快速
具体请见这篇:《golang第三方库fasthttp为什么要使用slice而不是map来存储header?》
避免栈逃逸golang中非常酷的一个语法特点就是没有堆和栈的区别 。编译器会自动识别哪些对象该放在堆上,哪些对象该放在栈上 。
func xxx() *ABigStruct{  a := new(ABigStruct)  // 看起来是在堆上的对象  var b ABigStruct      // 看起来是栈上的对象  // do something  // not return a   // a虽然是对象指针,但仅限于函数内使用,所以编译器可能把a放在栈上  return &b   // b超出了函数的作用域,编译器会把b放在堆上 。}valyala大神的经验:先找出程序的hot path,然后在hot path上做栈逃逸的分析 。尽量避免hot path上的堆内存分配,就能减轻GC压力,提升性能 。
fasthttp首页上的介绍:
Fast HTTP package for Go. Tuned for high performance. Zero memory allocations in hot paths. Up to 10x faster than net/http
这篇文章介绍了侦测栈逃逸的方法:
验证某个函数的变量是否发生逃逸的方法有两个:
  • go run -gcflags "-m -l" (-m打印逃逸分析信息,-l禁止内联编译);例:
?  testProj go run -gcflags "-m -l" internal/test1/main.go# command-line-argumentsinternal/test1/main.go:4:2: moved to heap: ainternal/test1/main.go:5:11: main make([]*int, 1) does not escape
  • go tool compile -S main.go | grep runtime.newobject(汇编代码中搜runtime.newobject指令,该指令用于生成堆对象),例:
?  testProj go tool compile -S internal/test1/main.go | grep newobject        0x0028 00040 (internal/test1/main.go:4) CALL    runtime.newobject(SB)

经验总结扩展阅读