前面的话
本篇针对的是 Go
语言常见的性能陷阱,通过 Go
自带的 Benchmark
进行性能测试。
Slice
预内存分配- 字符串处理
- 空结构体
struct{}{}
的使用
Benchmark
内存分配情况探索
测试文件为 _test.go
结尾的文件,测试其中函数签名格式如 func BenchmarkXXX(b *testing.B) { funcBody }
的函数。测试时在该文件夹下输入 go test -bench . -benchmem
即可显示测试函数的内存分配的统计信息。
以一个简单的 Slice
内存分配函数为例
func BenchmarkSlice(b *testing.B) {
size := 1000
for i := 0; i < b.N; i++ {
var s []int
for j := 0; j < size; j++ {
s = append(s, j)
}
}
}
命令行输入 go test -bench . -benchmem 得到以下结果
goos: windows
goarch: amd64
cpu: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz
BenchmarkSlice-8 283892 4397 ns/op 25208 B/op 12 allocs/op
前三行为系统信息无需分析。
BenchmarkSlice
为测试的函数名-8
为GOMAXPROCS
为测试的线程数283892
是总共执行的测试,即在函数测试中使用的b.N
的值4397 ns/op
是每次执行的平均操作时间25208/op
表示每次操作申请的内存12 allocs/op
为每次操作申请的内存次数
Slice
预内存分配
Go
语言中切片的底层是一个长度可管理的数组,在执行 slice = make([]T, m, n)
时,会分配一个长度为 m
,容量为 n
的数组,通过内置函数 len/cap
可以观察到 len(slice) = m / len(slice) = n
。
当对函数执行 append
扩容 x
长度时,如果 (n-m) > x
即剩余空间足够大,则在原数组上新添 x
个参数,并将 slice
对外显示长度改为 m+x
,即 len(slice) = m+x
, 但容量是不会变的即 cap(slice) = n
。
如果剩余空间不够大时,根据原容量大小再扩容:
- 当原容量
cap(slice) < 1024
时,会请求一个二倍空间的新数组。 - 当原容量
cap(slice) > 1024
时,会请求一个1.25倍空间的新数组。 - 以上两步循环执行至数组空间足够满足
append
函数的需求
因此预先为 Slice
分配出足够的容量是十分必要的。
func BenchmarkSlice(b *testing.B) {
size := 1000
for i := 0; i < b.N; i++ {
var s []int
for j := 0; j < size; j++ {
s = append(s, j)
}
}
}
func BenchmarkSliceWithPreAllocated(b *testing.B) {
size := 1000
for i := 0; i < b.N; i++ {
var s []int = make([]int, 0, size)
for j := 0; j < size; j++ {
s = append(s, j)
}
}
}
// go test bench . -benchmem
// output:
// BenchmarkSlice-8 283892 4397 ns/op 25208 B/op 12 allocs/op
// BenchmarkSliceWithPreAllocated-8 1844385 633.8 ns/op 0 B/op 0 allocs/op
字符串拼接
Go
语言中的字符串是常量,不可修改的,即使通过 []byte
强制转换,也只是获得一个复制的字节数组,因此字符串拼接的各种方式需要进行实验测试。常见的拼接方式如下所示
+
fmt.Sprintf
strings.Builder
bytes.Buffer
[]byte
const stringSize = 1000
func BenchmarkStringConcatPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < stringSize; j++ {
s += "a"
}
}
}
func BenchmarkStringConcatFmtSprintf(b *testing.B) {
for i := 0; i < b.N; i++ {
s := ""
for j := 0; j < stringSize; j++ {
s = fmt.Sprintf("%s%s", s, "a")
}
}
}
func BenchmarkStringConcatStringsBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
s := strings.Builder{}
for j := 0; j < stringSize; j++ {
s.WriteString("a")
}
_ = s.String()
}
}
func BenchmarkStringConcatStringsBuilderWithPreAllocated(b *testing.B) {
for i := 0; i < b.N; i++ {
s := strings.Builder{}
s.Grow(stringSize)
for j := 0; j < stringSize; j++ {
s.WriteString("a")
}
_ = s.String()
}
}
func BenchmarkStringConcatBytesBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
s := bytes.Buffer{}
for j := 0; j < stringSize; j++ {
s.WriteString("a")
}
_ = s.String()
}
}
func BenchmarkStringConcatBytesBufferWithPreAllocated(b *testing.B) {
for i := 0; i < b.N; i++ {
s := bytes.Buffer{}
s.Grow(stringSize)
for j := 0; j < stringSize; j++ {
s.WriteString("a")
}
_ = s.String()
}
}
func BenchmarkStringConcatByteSlice(b *testing.B) {
for i := 0; i < b.N; i++ {
var s []byte
for j := 0; j < stringSize; j++ {
s = append(s, []byte("a")...)
}
_ = string(s)
}
}
func BenchmarkStringConcatByteSliceWithPreAllocated(b *testing.B) {
for i := 0; i < b.N; i++ {
var s = make([]byte, 0, stringSize)
for j := 0; j < stringSize; j++ {
s = append(s, []byte("a")...)
}
_ = string(s)
}
}
// output:
// BenchmarkStringConcatPlus-8 8773 121408 ns/op 530275 B/op 999 allocs/op
// BenchmarkStringConcatFmtSprintf-8 4812 218469 ns/op 546536 B/op 1998 allocs/op
// BenchmarkStringConcatStringsBuilder-8 546016 2410 ns/op 3320 B/op 9 allocs/op
// BenchmarkStringConcatStringsBuilderWithPreAllocated-8 752048 1687 ns/op 1024 B/op 1 allocs/op
// BenchmarkStringConcatBytesBuffer-8 206568 5390 ns/op 3248 B/op 6 allocs/op
// BenchmarkStringConcatBytesBufferWithPreAllocated-8 246604 4780 ns/op 2048 B/op 2 allocs/op
// BenchmarkStringConcatByteSlice-8 650638 1608 ns/op 4344 B/op 10 allocs/op
// BenchmarkStringConcatByteSliceWithPreAllocated-8 1501183 822.5 ns/op 1024 B/op 1 allocs/op
+
与fmt.Sprintf
最低效,因为fmt.Sprintf
本身就是用于格式字符串,而+
则是低效的不断申请新的内存空间。[]byte
最快, 因为没有任何其他管理消耗。strings.Builder
与bytes.Buffer
相近,但也有两倍的差距。实际上底层也是在管理[]byte
,不过还有其他管理参数并实现了一些管理方法。 两种方法的底层实现也基本相近,但在转换成字符串时strings.Builder
是直接将[]byte
数组强制转换为string
, 而bytes.Buffer
方法则是创建新内存转换为string
。
空结构体 struct{}{}
的使用
fmt.Println(unsafe.Sizeof(struct{}{}), unsafe.Sizeof(true), unsafe.Sizeof(false))
// output:
// 0 1 1
- 空结构体在
Go
语言中不占用任何额外空间- 在需要
Set
集合时,即使使用布尔值也额外需要一个字节的空间,而空结构体不需要
- 在需要
- 空结构体本身具有很强的语义性,即表示不需要任何值,仅作为占位符
- 在
chan
传输数据时,空结构体既不占用空间也不会造成二义性
- 在