入门 Go 语言-高质量编程与性能调优(二)| 青训营笔记

53 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 5 天

入门 Go 语言-高质量编程与性能调优(二)

性能调优实战

性能调优简介

简介

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对文
  • 针对Go语言特性,介绍Go相关的性能优化建议

性能优化建议-Benchmark

如何使用

  • 性能表现需要实际数据衡量
  • Go语言提供了支持基准性能测试的benchmark工具 go test -bench. -benchmem fib.go
func Fib(n int) int {
   if n < 2 {
      fmt.Print(n)
      return n
   }
   return Fib(n-1) + Fib(n-2)
}

fib_test.go

func BenchmarkFib10(b *testing.B) {
   for i := 0; i < b.N; i++ {
      Fib(10)
   }
}

slice预分配内存

  • 尽可能在使用make()初始化切片时提供容量信息
func NoPreAlloc(size int) {
   data := make([]int, 0)
   for i := 0; i < size; i++ {
      data = append(data, i)
   }
}
func PreAlloc(size int) {
   data := make([]int, 0, size)
   for i := 0; i < size; i++ {
      data = append(data, i)
   }
}
  • BenchmarkNoPreAlloc-12 5632498 205.5 ns/op
  • BenchmarkPreAlloc-12 25896112 46.65 ns/op
  • 切片本质是一个数组片段的描述
    • 包括数组指针
    • 片段的长度
  • 片段的容量(不改变内存分配情况下的最大长度)。切片操作并不复制切片指向的元素
  • 创建一个新的切片会复用原来切片的底层数组

另一个陷阱:大内存未释放

  • 在已有切片基础上创建切片,不会创建新的底层数组
  • 场景
    • 原切片较大,代码在原切片基础上新建小切片
    • 原底层数组在内存中有引用,得不到释放
  • 可使用copy替代re-slice
func GetLastBySlice(origin []int) []int {
   return origin[len(origin)-2:]
}
func GetLastByCopy(origin []int) []int {
   result := make([]int, 2)
   copy(result, origin[len(origin)-2:])
   return result
}

map预分配内存

  • 不断向map中添加元素的操作会触发map的扩容
  • 提前分配好空间可以减少内存拷贝和 Rehash的消耗
  • 建议根据实际需求提前预估好需要的空间
func NoPreAlloc(size int) {
   data := make(map[int]int)
   for i := 0; i < size; i++ {
      data[i] = 1
   }
}

func PreAlloc(size int) {
   data := make(map[int]int, size)
   for i := 0; i < size; i++ {
      data[i] = 1
   }
}

test

func BenchmarkNoPreAlloc(b *testing.B) {
   for i := 0; i < b.N; i++ {
      NoPreAlloc(30)
   }
}

func BenchmarkPreAlloc(b *testing.B) {
   for i := 0; i < b.N; i++ {
      PreAlloc(30)
   }
}
  • BenchmarkNoPreAlloc-12 527054 2160 ns/op 2218 B/op 6 allocs/op
  • BenchmarkPreAlloc-12 1280247 896.8 ns/op 1166 B/op 1 allocs/op

性能优化建议-字符串处理

  • 常见的字符串拼接方式
    func Plus(n int, srt string) string {
       s := ""
       for i := 0; i < n; i++ {
          s += srt
       }
       return s
    }
    func StrBuilder(n int, srt string) string {
       var builder strings.Builder
       for i := 0; i < n; i++ {
          builder.WriteString(srt)
       }
       return builder.String()
    }
    func ByteBuffer(n int, srt string) string {
       buf:=new(bytes.Buffer)
       for i := 0; i < n; i++ {
          buf.WriteString(srt)
       }
       return buf.String()
    }
    
  • BenchmarkPlus-12 160256 8157 ns/op 15992 B/op 99 allocs/op
  • BenchmarkStrBuilder-12 1951412 591.1 ns/op 1016 B/op 7 allocs/op
  • BenchmarkByteBuffer-12 1000000 1010 ns/op 1280 B/op 5 allocs/op

使用strings.Builder

  • 使用+拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer 更快
  • 分析
    • 字符串在Go语言中是不可变类型,占用内存大小是固定的
    • 使用+每次都会重新分配内存
    • strings.Builder,bytes.Buffer底层都是[]byte 数组
    • 内存扩容策略,不需要每次拼接重新分配内存
  • bytes.Buffer转化为字符串时重新申请了一块空间
  • strings.Builder直接将底层的[]byte转换成了字符串类型返回
func PreStrBuilder(n int, srt string) string {
   var builder strings.Builder
   builder.Grow(n * len(srt))
   for i := 0; i < n; i++ {
      builder.WriteString(srt)
   }
   return builder.String()
}
func PreByteBuffer(n int, srt string) string {
   buf := new(bytes.Buffer)
   buf.Grow(n * len(srt))
   for i := 0; i < n; i++ {
      buf.WriteString(srt)
   }
   return buf.String()
}
  • BenchmarkPlus-12 2275 487942 ns/op 1602943 B/op 999 allocs/op
  • BenchmarkStrBuilder-12 259966 4420 ns/op 8440 B/op 11 allocs/op
  • BenchmarkByteBuffer-12 127496 8870 ns/op 11200 B/op 8 allocs/op
  • BenchmarkPreStrBuilder-12 296757 4454 ns/op 3072 B/op 1 allocs/op
  • BenchmarkPreByteBuffer-12 183508 6612 ns/op 6144 B/op 2 allocs/op

性能优化建议-空结构体

使用空结构体节省内存

  • 空结构体struct 实例不占据任何的内存空间
  • 可作为各种场景下的占位符使用
    • 节省资源
    • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {
   m := make(map[int]struct{})
   for i := 0; i < n; i++ {
      m[i] = struct{}{}
   }
}
func BoolMap(n int) {
   m := make(map[int]bool)
   for i := 0; i < n; i++ {
      m[i] = false
   }
}
  • BenchmarkEmptyStructMap-12 35041 34217 ns/op 23887 B/op 37 allocs/op
  • BenchmarkBoolMap-12 33301 35685 ns/op 26643 B/op 48 allocs/op
  • 实现Set,可以考虑用map来代替
  • 对于这个场景,只需要用到map的键,而不需要值
  • 即使是将map 的值设置为bool类型,也会多占据1个字节空间

性能优化建议-atomic包

如何使用atomic包

  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic操作是通过硬件实现,效率比锁高
  • sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值操作,可以使用atomic.Value,能承载一个interface0
type atomicCounter struct {
   i int32
}

func AtomicAddOne(c *atomicCounter)  {
   atomic.AddInt32(&c.i,1)
}

type mutexCounter struct {
   i int32
   m sync.Mutex
}

func MutexAddOne(c *mutexCounter)  {
   c.m.Lock()
   c.i++
   c.m.Unlock()
}
  • BenchmarkAtomicAddOne-12 75900723 14.74 ns/op 4 B/op 1 allocs/op
  • BenchmarkMutexAddOne-12 36162004 29.30 ns/op 16 B/op 1 allocs/op

性能分析工具 pprof 实战

性能调优原则

  • 要依靠数据不是猜测
  • 要定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化

说明

  • 希望知道应用在什么地方耗费了多少 CPU、Memory
  • pprof是用于可视化和分析性能分析数据的工具
  • pprof 功能简介

image.png

  • pprof 排查实战

搭建pprof 实践项目

  • GitHub(来自Wolfogre)
  • 项目提前埋入了一些炸弹代码,产生可观测的性能问题
  • pprof 的采样过程和原理

浏览器查看指标

image.png

CPU go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"

Saved profile in C:\Users\yayuke\pprof\pprof.samples.cpu.001.pb.gz
Type: cpu
Time: Jan 17, 2023 at 10:18pm (CST)
Duration: 10.18s, Total samples = 2.75s (27.01%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

命令:topN-查看占用资源最多的函数

  • flat当前函数本身的执行耗时
  • flat%flat占CPU总时间的比例
  • sum%上面每一行的flat%总和
  • cum指当前函数本身加上其调用函数的总耗时
  • cum%cum占CPU总时间的比例 image.png
  • Flat == Cum,函数中没有调用其他函数
  • Flat == 0,函数中只有其他函数的调用 命令:list-根据指定的正则表达式查找代码行 image.png 命令:web-调用关系可视化

Heap-堆内存 go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/heap"

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

goroutine-协程

  • goroutine泄露也会导致内存泄露

image.png go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

  • 由上到下表示调用顺序
  • 每一块代表一个函数,越长代表占用CPU的时间更长
  • 火焰图是动态的,支持点击块进行分析

mutex-锁 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" block-阻塞 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"