性能调优 | 青训营笔记

74 阅读3分钟

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

性能测试

go benchmark 性能测试

条件

  1. 文件需要以_test.go结尾
  2. 方法需要以Benchmark开头
  3. 方法参数需要有b *testing.B
  4. 方法无返回值
  5. 运行命令go test -bench . -benchmem

执行的结果即各个参数的含义

image.png

案例

案例一:slice预分配

package NoPreAlloc

import "testing"

func BenchmarkNoPreAlloc(b *testing.B) {
   b.ResetTimer()
   for n := 0; n < b.N; n++ {
      NoPreAlloc(b.N)
   }
}
func BenchmarkPreAlloc(b *testing.B) {
   b.ResetTimer()
   for n := 0; n < b.N; n++ {
      PreAlloc(b.N)
   }
}
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预分配内存,对于每次执行花费的时间是差不多的,但是对于内存的使用和内存的申请次数,为slice预分配内存会少很多。

原因:

因为若没有预分配内存,则会默认给一个容量大小,然后在添加元素时,若容量不够,则该slice会进行扩容,会将原来的数据copy到一个更大的容量的slice,然后将新加的值加到该新的slice中。此过程,会对内存进行一系列的操作

同理,map也是类似的。

案例二:strings.Builder

package NoPreAlloc

import (
	"bytes"
	"strings"
	"testing"
)

func BenchmarkPlus(b *testing.B) {
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		Plus(b.N, "a")
	}
}
func BenchmarkStrBuilder(b *testing.B) {
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		StrBuilder(b.N, "a")
	}
}
func BenchmarkByteBuffer(b *testing.B) {
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		ByteBuffer(b.N, "a")
	}
}
func Plus(n int, str string) string {
	s := ""
	for i := 0; i < n; i++ {
		s += str
	}
	return s
}
func StrBuilder(n int, str string) string {
	var builder strings.Builder
	// 可使用与分配的方式
    // builder.Grow(n * len(str))
	for i := 0; i < n; i++ {
		builder.WriteString(str)
	}
	return builder.String()
}
func ByteBuffer(n int, str string) string {
	buf := new(bytes.Buffer)
    // 可使用与分配的方式
	// buf.Grow(n * len(str))
	for i := 0; i < n; i++ {
		buf.WriteString(str)
	}
	return buf.String()
}

结果:

image.png

结论:

strings.Builder 最快,bytes.Buffer 较快,+ 最慢

原理:

  • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和

  • strings.Builder,bytes.Buffer 的内存是以倍数申请的

  • strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回

  • 如果事先知道需要的内存大小的话,可以对内存进行预分配,builder.Grow(n * len(str))

优化建议

使用空结构体节省内存

  • 空结构体struct{}市里不占据任何的内存空间

  • 可作为各种场景下的占位符使用

    • 节省资源
    • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符

使用场景

比如实现简单的 Set

Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。对于集合场景,只需要用到 map 的键而不需要值

使用 atomic 包

使用atomic包会比加锁好很多。如果是只维护一个值的话,可以使用atomic 包,若需要维护一段逻辑的时候,才考虑使用锁机制

性能调优原则

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

性能调优工具

pprof工具

使用: 实用go pprof使用指南

总结

今天学习的内容主要是对于性能的测试与调优。了解到了 benchmark的使用,并结合一个slice,一个字符串拼接这两个案例去深入 benchmark,和对一些代码设计的分析。同时还学习到了对于pprof工具的简单调优使用。