这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
性能测试
条件
- 文件需要以
_test.go结尾 - 方法需要以
Benchmark开头 - 方法参数需要有
b *testing.B - 方法无返回值
- 运行命令
go test -bench . -benchmem
执行的结果即各个参数的含义
案例
案例一: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)
}
}
结果:
分析:
可以看出来,有没有提前为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()
}
结果:
结论:
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工具的简单调优使用。