性能优化指南
- 简介
- 性能优化的前提是满足正确可靠、简介清晰等质量因素
- 性能有话是综合评估,有时候时间效率和空间效率可能对立
- 针对 Go 语言特性,介绍 Go 相关的性能优化建议
1 Benchmark
- 如何使用
- 性能表现需要实际数量衡量
- Go 语言提供了支持基准性能测试的 benchmark 工具
- 补充
- import "testing"
- 在终端目录文件夹下输入命令
go mod init + 模块名称用来初始化 go.mod 文件,否则会无法使用 benckhmark 工具 - 使用时可直接在命令行输入
go test -bench. -benchmem如下.png)
- 或者在 vscode 中点击 run benchmark
- 都可得到如下结果
- BenchmarkPreAlloc 是测试的函数名
- -20 表示GOMAXPROCS(CPU核数)的值为20
- 54102753 表示一共执行54102753次 即b.N的值
- 22.48 ns/op 每次执行花费22.48ns
- 80 B/op 每次执行申请多大的内存
- 1 allocs/op 每次执行申请几次内存
2 Slice
- slice 预分配内存
- 尽可能在使用 make() 初始化切片时提供容量信息
- 切片本质是一个数组片段的描述
- 包括数组指针
- 片段的长度
- 片段的容量(不改变内存分配情况下的最大长度)
type slice struct{ array unsafe.Pointer len int cap int } - 切片操作并不复制切片指向的元素
- 创建一个新的切片会复用原来切片的底层数组
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) } }
3 Map
- map 预内存分配
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
- 建议根据实际需求提前预估好需要的空间
func NoPreAlloc(size int) { data := make(map[int]int) 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 } }
4 String
- 使用 strings.Builder
- 常见的字符串拼接方式
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 for i := 0; i < n; i++ { builder.WriteString(str) } return builder.String() }func ByteBuffer(n int, str string) string { buf := new(bytes.Buffer) for i := 0; i < n; i++ { buf.WriteString(str) } return buf.String() }func PreStrBuilder(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 PreByteBuffer(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() }- 使用 + 拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer 更快
- 字符串在 G0 语言中是不可变类型,占用内存大小是固定的
- 使用 + 每次都会重新分配内存
- strings.Builder,bytes.Buffer 底层都是 []byte 数组
- 内存扩容策略,不需要每次拼接重新分配内存
- bytes.Buffer 转化为字符串时重新申请了一块空间
- strings.Builder 直接将底层的 []byte 转换成了字符串类型返回
5 空结构体
- 使用空结构体节省内存
- 空结构体 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 } }- 实现 Set,可以考虑用 map 来代替
- 对于这个场景,只需要用到 map 的键,而不需要值
- 即使是将 map 的值设置为 bool 类型,也会多占据1个字节空间
一个开源实现:github.com/deckarep/go…
6 atomic 包
- 如何使用 atomic 包
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() }- 锁的实现是通过操作系统来实现,属于系统调用
- atomic 操作是通过硬件实现,效率比锁高
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}
小结
- 避免常见的性能陷阱可以保证大部分程序的性能
- 普通应用代码,不要一味地追求程序的性能
- 越高级的性能优化手段越容易出现问题
- 在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能