GO性能调优 | 青训营笔记

58 阅读1分钟

这是我参加[第五届青训营]伴学笔记创作活动的第 4 天

零、 前言

本文整理了一些关于性能调优的知识点,撰写本文的目的一是为了参加伴学笔记创作活动,而是方便自己未来查缺补漏。

一、本堂课重点内容

  • 性能调优

    • 性能优化指南
    • 性能优化分析工具
    • 性能调优实战案例

二、详细知识点回顾

2.1 性能优化指南

简介:

1.性能优化的前提是代码满足正确可靠、简介清晰的基本要求

2.性能优化是综合评估,有时候时间效率和空间效率会对立

Benchmark

如下形式的函数

func BenchmarkXxx(*testing.B)

被认为是基准测试,需要通过go test命令执行,其基本形式为:

go test [build/test flags] [packages] [build/test flags & test binary flags]

基准函数测试样例:

// from fib.go
func Fib(n int) int {
   if n <= 2 {
      return n
   }
   return Fib(n-1) + Fib(n-2)
}

// from fib_test.go
```
func BenchmarkFib10(b *testing.B) {
   for n := 0; n < b.N; n++ {
      Fib(10)
   }
}
```

执行go test命令,它会在它会在 *_test.go 中寻找 test 测试benchmark 基准 和 examples 示例 函数,其中基准函数必须以Benchmarkxxx的形式命名。

命令go test -bench=. -benchmem

运行结果

image.png

Slice

Slice数据结构

type Slice struct {
   array unsafe.Pointer
   len int
   cap int
}

尽可能使用make()初始化切片时提供容量信息,这样会为Slice预分配内存,在Slic容量足够的情况下,对其追加元素不会引起扩容,而扩容涉及内存操作,需要消耗一定时间

image.png

考虑以下场景

  • 原切片较大,代码在原切片基础上创建了小切片
  • 原底层数组在内存有引用,得不到释放
func f(origin []int) []int {
   return origin[len(origin)-2 :]
}

在该实例函数中返回origin的切片,origin底层数组在源程序中有引用,导致origin底层数组不会被垃圾回收机制释放,占用大量内存(假设origin很大)

func f(origin []int) []int {
   result := make([]int, 2)
   copy(result, origin[len(origin)-2:])
   return result
}

改用copy函数可以完美解决这个问题,这样origin会在该函数完成后被释放

Map

预分配内存,根据实际需求分配长度,可以对比Slice,两者大同小异

字符串处理

使用+拼接字符串 VS 使用strings.Builder VS 使用ByteBuffer

image.png

image.png

结果:使用+拼接字符串性能最差,使用strings.Builder和使用ByteBuffer相差不大,strings.Builder稍快一些,这是由于bytes.Buffer转化为字符串是重新申请了一片内存空间

在预知字符串长度的时候,可以使用Grown方法对字符串预分配内存,提高性能

image.png

struct空结构体

空结构体struct{}不占用任何内存,可作为占位符使用,最常用的方式是与Map一起实现set,如map[int]struct{}

atomic包

使用atomic包 VS 使用加锁

image.png

锁的实现依赖操作系统,调用成本偏高,锁的使用因该用来保护一段逻辑,而不是保护一个数据项。

2.2 性能优化工具pprof

pprof是用于可视化和分析性能分析数据的工具,可以用pprof检查你的Go程序在什么地方占用的Cpu或memory最多

image.png

性能调优实战案例

准备:首先克隆我们需要用到的项目代码,使用Git bashgit clone https://github.com/wolfogre/go-pprof-practice,保证项目可以编译运行

项目main包:

package main

import (
   "log"
   "net/http"
   _ "net/http/pprof"
   "os"
   "runtime"
   "time"

   "github.com/wolfogre/go-pprof-practice/animal"
)

func main() {
   log.SetFlags(log.Lshortfile | log.LstdFlags)
   log.SetOutput(os.Stdout)

   runtime.GOMAXPROCS(1)
   runtime.SetMutexProfileFraction(1)
   runtime.SetBlockProfileRate(1)

   go func() {
      if err := http.ListenAndServe(":6060", nil); err != nil {
         log.Fatal(err)
      }
      os.Exit(0)
   }()

   for {
      for _, v := range animal.AllAnimals {
         v.Live()
      }
      time.Sleep(time.Second)
   }
}

项目运行时,使用浏览器打开http://localhost:6060/debug/pprof/

image.png

注意保证你的项目处于运行状态!

  • CPU

打开终端运行命令go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"

image.png

输入top

image.png

flat 当前函数的执行耗时

flat% flat占CPU总时间的比例

sum% 上面每一项的flat%总和

cum 指的是本函数加上其调用函数的总耗时

cum% cum占CPU总时间的比例

细心的你思考一下,什么时候flat == cum?,什么时候flat == 0? 当前函数没有调用其他函数 flat == cum 当前函数只调用其他函数,而本身并不占用CPU时间 flat == 0

可以看到函数Eat占用了最多的CPU时间,命令list通过正则式查找代码行

image.png

其中第24行的for循环占用的绝大部分CPU时间,把这段代码注释掉(做性能优化)

命令web可以使调用关系可视化:

image.png

优化CPU之后,我们转而优化内存占用

  • Heap-堆内存

我们将优化CPU后的程序重新运行,打开终端输入命令 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"

image.png

pprof自动使用图形化界面展示占用内存的比例,可以看到包mouse中的函数steal占用绝大部分内存,转而排查该函数:

image.png

alloc_objects 程序累计申请对象数 alloc_space 程序当前持有对象数 inuse_objects 程序累计申请内存大小 inuse_space 程序当前占用的内存大小

image.png

func (m *Mouse) Steal() {
   log.Println(m.Name(), "steal")
   max := constant.Gi
   for len(m.buffer)*constant.Mi < max {
      m.buffer = append(m.buffer, [constant.Mi]byte{})
   }
}

可以看到该函数持续的申请内存,将这段代码注释掉(优化),重新运行,可以看到内存占用问题已经大大改善了。

  • goroutine

运行程序,终端输入命令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

image.png

可以点击页面viem -> flame Graph转而观察程序的火焰图,Drink占用了绝大部分协程,转向source视图,搜索Drink看看它到底做了什么

image.png

可以看到它在routine中等待30s才退出,将相关代码注释掉(优化)

  • block

运行程序,终端输入命令go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"

image.png

然后排查发生block的两个函数,基本步骤和上面类似

  • 总结

image.png