Go性能测试和优化建议
1. Go性能测试benchmark工具
Go语言中提供了支持基准性能测试的benchmark工具。
go test -bench=. -benchmem
benchmark的测试代码如下:
- 测试代码文件必须以_test.go结尾
- 测试函数必须以Benchmark为开头
- Benmark函数的参数必须是*testing.B
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
// BenchmarkFib10 run 'go test -bench=. -benchmem' to get the benchmark result
func BenchmarkFib10(b *testing.B) {
// run the Fib function b.N times
for n := 0; n < b.N; n++ {
Fib(10)
}
}
go test -bench=. -benchmem测试的结果如下:
2. 性能优化的建议(方法)
2.1 slice预分配内存
- 在使用make()初始化的时候提供预分配的容量信息,这样可以避免底层因为数组的扩容而频繁申请和复制内存。如果不进行预分配内存的话,在不段添加新元素的时候,会造成数组的频繁扩容,扩容的时候需要申请一片更大的内存空间,将原来切片的内容复制到新的内存空间中。
测试的代码如下:NoPreAlloc函数和PreAlloc函数分别表示没有预分配内存和有预分配内存的情况。
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在进行预分配容量比不进行预分配容量的性能提升了不少。
- 在进行切片的截取的时候,大的切片(占内存空间大的切片)被小的切片所截取的时候,因为大切片和小切片共用底层数组,所以在小切片还在使用的时候大切片不能被及时的回收,这时会造成一定程度的内存泄漏。解决办法:在进行截取小切片的时候可以采用copy函数,这样可以保证大切片可以及时的回收。
如下的代码展示了两种切片截取的方式:前者采用索引进行截取(共用底层数组),后者采用copy函数进行截取。提示:origin切片是一个很大的切片
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
}
采用如下的命令查看内存的占用情况:
go test -run=. -v
测试的结果如下图所示:
从上图可以看出,直接对大的切片进行截取小切片的内存开销远远大于采用copy函数
2.2 map预分配内存
采用make()函数创建map的时候,应该尽可能的传入容量的参数,给map预分配一定大小的内存空间。提前分配好一定的内存空间可以减少内存拷贝和Rehash的开销。
2.3 在进行字符串的拼接的时候采用strings.Bulider
- 拼接字符串采用+的方式拼接的性能最差:原因是因为字符串在Go语言中是不可变的类型,占用的内存大小固定,每次使用+拼接字符串都会重新开辟一段新的内存空间将旧的字符串复制到新的内存空间中。
- 使用strings.Bulider的性能最优,其次是bytes.Buffer。原因是:strings.Builder和bytes.Buffer的底层都是[]byte数组,在传入字符串的时候,并不是每次都会进行扩容操作。
测试的代码如下所示:第一个函数表示采用+的方式进行拼接字符串,第二种方式为采用strings.Bulider,第三种方式为采用bytes.Buffer。
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()
}
性能测试的结果:
如上图可知,在进行字符串的拼接的时候,strings.Bulider的性能最优,采用+号进行拼接的方法性能最差,bytes.Buffer性能次之。
2.4 使用空结构体
空结构体不占用内存空间,可以在channel中用于传递信号,也可以用在map的value中,表示称hashset。空结构体可以节省系统的内存空间。