1.5 性能优化建议
1.5.1 Go基准性能测试工具: benchmark
b *testing.B表示benchmark测试
指令:go test -bench=. -benchmem filename ,点表示当前package
➜ lessonThree go test -bench=. -benchmem fib_benchmark_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i5-1038NG7 CPU @ 2.00GHz
BenchmarkFib10-8 3949941 307.6 ns/op 0 B/op 0 allocs/op
PASS
ok command-line-arguments 1.886s
· BenchmarkFib10-8表示测试名为BenchmarkFib10,-8为GOMAXSPROCX的值为8,该值在v>1.5后默认为CPU核数
CPU核数可通过-CPU=指定,且支持列表如-CPU=2,4
· 3949941表示b.N的值,即执行次数
b.N的确定:初始值为1,如果一次运行能够在1s内完成,则增加该值后继续执行,大概是以不到2倍的速度增加。
· 307.6 ns/op为每次执行花费时间,0 B/op为每次执行申请内存大小,0 allocs/op为每次执行申请内存次数
1.5.2 slice
- 预分配内存
尽可能在make()初始化切片时提供容量信息,提前分配消耗时间为原来的1/3左右
/*
BenchmarkPreAlloc-8 139949324 9.056 ns/op 44 B/op 0 allocs/op
PASS
ok command-line-arguments 3.341s
*/
func NoPreAlloc(size int) {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
/*
BenchmarkPreAlloc-8 303202378 3.694 ns/op 8 B/op 0 allocs/op
PASS
ok command-line-arguments 2.061s
*/
func PreAlloc(size int) {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
slice底层数据结构:对数组片段的描述,包含数组指针、片段长度(len)、片段容量(cap)等。
切片操作不复制原来元素,复用原来底层数组:对slice的操作回导致数组的变化
copy代替re-slice
存在问题:基于提到的slice的特点,如果原始切片较大,创造了小的切片,在使用过程中原始数组存在引用,一直不会被释放。
代码如下:
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
}
func generateWithCap(n int) []int {
rand.Seed(time.Now().UnixNano())
nums := make([]int, 0, n)
for i := 0; i < n; i++ {
nums = append(nums, rand.Int())
}
return nums
}
func printMem(t *testing.T) {
t.Helper()
var rtm runtime.MemStats
runtime.ReadMemStats(&rtm)
t.Logf("%.2f MB", float64(rtm.Alloc)/1024./1024.)
}
测试结果如下:
➜ lessonThree go test -run=. -v
=== RUN TestLastCharsBySlice
get_last_test.go:43: 100.14 MB
--- PASS: TestLastCharsBySlice (0.28s)
=== RUN TestLastCharsByCopy
get_last_test.go:43: 3.14 MB
--- PASS: TestLastCharsByCopy (0.26s)
PASS
ok lessonThree 1.134s
明显可以看到,使用copy方法节省了大量内存。
优化:显式地调用GC进行空间回收runtime.GC(),结果如下
➜ lessonThree go test -run=. -v
=== RUN TestLastCharsBySlice
get_last_test.go:44: 100.14 MB
--- PASS: TestLastCharsBySlice (0.31s)
=== RUN TestLastCharsByCopy
get_last_test.go:44: 0.15 MB
--- PASS: TestLastCharsByCopy (0.28s)
PASS
ok lessonThree 1.397s
1.5.3 map
-
预分配内存
- 不断向map中添加元素的操作会触发map扩容的判断和操作
- 提前分配好空间可以减少内存的拷贝和rehash的消耗
- 建议根据实际需求提前预估好需要的空间
- 思考:map和slice内存空间的处理和redis相似
1.5.4 string
-
对比使用
+,string.Builder,bytes.Buffer的情况,结果如下:BenchmarkPlus-8 22 58738147 ns/op 530997411 B/op 10021 allocs/op BenchmarkStrBuilder-8 11007 110900 ns/op 505842 B/op 24 allocs/op BenchmarkByteBuffer-8 9998 115830 ns/op 423538 B/op 13 allocs/op分析:字符串在go中是不可变类型,大小固定。
使用
+性能最差,因为每次都会重新分配内存。string.Builder,bytes.Buffer差不多,底层都是[]byte数组。strings.Buffer更快,最快的应该是preByteConcat。因为这几个在分配内存时都是以2的指数分配,有一定预留,可减少分配次数。strings.Builder和bytes.Buffer底层都是[]byte数组,但strings.Builder性能比bytes.Buffer略快约 10% 。一个比较重要的区别在于,bytes.Buffer转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder直接将底层的[]byte转换成了字符串类型返回了回来。func preByteConcat(n int, str string) string { buf := make([]byte, 0, n*len(str)) for i := 0; i < n; i++ { buf = append(buf, str...) } return string(buf) } -
综合一般使用
string.Builder。并且`strBuilder和byteBuffer都提供了预分配内存的方式Grow(),优化后:func StrBuilderWithGrow(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 ByteBufferWithGrow(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() }BenchmarkPlus-8 22 53229208 ns/op 530997206 B/op 10020 allocs/op BenchmarkStrBuilder-8 10000 108898 ns/op 505842 B/op 24 allocs/op BenchmarkStrBuilderWithGrow-8 24224 49554 ns/op 106496 B/op 1 allocs/op BenchmarkByteBuffer-8 9909 104994 ns/op 423537 B/op 13 allocs/op BenchmarkByteBufferWithGrow-8 16585 76483 ns/op 212993 B/op 2 allocs/op
1.5.5 struct
-
使用空结构体节省内存,空结构体实例不占用任何内存空间
func main() { fmt.Println(unsafe.Sizeof(struct{}{})) } /* 0 */ -
核心作用就是作为占位符使用 节省空间 本身就具备很强的语义
-
实现Set
通常用map来代替,但其实只需要key值,因此浪费了空间。因此,用struct实现set,可将value设为空
type Set map[string]struct{} func (s Set) Has(key string) bool { _, ok := s[key] return ok } func (s Set) Add(key string) { s[key] = struct{}{} } func (s Set) Delete(key string) { delete(s, key) } func main() { s := make(Set) s.Add("Tom") s.Add("Sam") fmt.Println(s.Has("Tom")) fmt.Println(s.Has("Jack")) }更完整的开源实现
-
不发送数据的信道(channel)
只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度
func worker(ch chan struct{}) { <-ch fmt.Println("do something") close(ch) } func main() { ch := make(chan struct{}) go worker(ch) ch <- struct{}{} }
-
1.5.6 atomic包
应用于多线程的时候
相较于mutex使用系统调用加锁,atomic使用硬件,速度更快,效率更高。
mutex应适用于保护一段逻辑而不仅仅是一个变量。