Go语言中的性能优化 | 青训营笔记

107 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。

性能优化,简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。

如何评估性能

在Go语言标准库内置的testing测试包中,包含有基准测试(Benchmark)的功能,可以方便地进行性能测试。其中基准测试需要如下几个条件:

  1. 同其他测试文件一样,基准测试代码的文件名需要以_test.go结尾。
  2. 基准测试的函数必须以Benchmark开头。
  3. 基准测试函数需要接收一个指向testing.B变量的指针。
  4. 测试的代码需要放在一个循环次数为b.N次的循环中(b为上文中指向testing.B变量的指针)。

一颗例子

我们对一个计算第N个斐波那契数为例:

// fib.go
package benchmark

func Fib(n int) int {
   if n < 2 {
      return n
   }
   return Fib(n-1) + Fib(n-2)
}

接下来,我们创建一个fib_test.go文件对此函数进行基准测试:

package benchmark

import "testing"

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

使用go test -benchmark=.即可进行性能测试,可得到输出如下:

image.png

前面几行说明了运行的环境以及以及对应的package,最后三行则说明了测试的情况,特别是倒数第二行,我们的测试函数名为BenchmarkFib10,其后的-12则说明了我们的GOMAXPROCS,在没有特定设置的情况下,这个值同我们CPU的核数相同;第二个数表明了我们一共执行了4778167次,可认为是b.N的值;第三个数表明我们每次执行这个函数花费了242.2ns。如此一来,我们便能够从一定程度上了解到我们性能如何。

Slice

预分配内存

尽可能在使用slice时,使用make()预分配一定内存空间。

func NoPreAlloc(size int) {
   data := make([]int, 0)
   for k := 0; k < size; k++ {
      data = append(data, k)
   }
}

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

image.png

可以看到预分配内存空间的Slice,能够花费更少时间。

使用Copy代替Re-Slice

在已有的切片基础上创建新的切片,不会影响新的底层数组。当我们存在一个相对较大的Slice,需要在此Slice上截取一个较小的Slice时,如果我们直接使用截取的方法,会导致原来较大的Slice因其上有引用,无法释放此较大的空间。因而我们需要使用Copy来替代Re-Slice。

Map

预分配内存

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

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

原理同Slice类似,是由于向Map中添加元素,会触发Map的扩容,导致添加了扩容的费时,预分配好内存空间可以减少扩容的次数。

image.png

字符串

使用strings.Builder进行字符串处理

func Plus(n int, str string) string {
   s := ""
   for i := 0; i < n; i++ {
      s += str
   }
   return s
}

func BuilderPlus(n int, str string) string {
   var builder strings.Builder
   for i := 0; i < n; i++ {
      builder.WriteString(str)
   }
   return builder.String()
}

func BufferPlus(n int, str string) string {
   buf := new(bytes.Buffer)
   for i := 0; i < n; i++ {
      buf.WriteString(str)
   }
   return buf.String()
}

image.png

可以看到使用+性能最差,strings.Builderbytes.Buffer性能相近,但是bytes.Buffer更快。

由于在Go语言中,字符串是不可变更的数据类型,使用+时,都需要进行内存扩容,导致效率低下。