性能优化建议| 青训营笔记

103 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

Benchmark的使用

Benchmark(基准测试)是在一定的工作条件之下检测程序性能的一种方法。

使用示例

fib.go

 package fib
 ​
 func fib(n int) int {
 if n <= 1 {
 return 1
 }
 return fib(n-1) + fib(n-2)
 }

fib_test.go

 package fib
 import "testing"
 ​
 func BenchmarkFib(b *testing.B) {
 for i := 0i < b.Ni++ {
 fib(10)
 }
 }

运行测试

go test -bench=. -benchmem

测试结果

 goos: darwin
 goarch: arm64
 pkg: fib
 BenchmarkFib-8 6039063 181.3 ns/op 0 B/op 0 allocs/op
 PASS
 ok fib 1.405s

go test命令参数:

  • -bench regexp 运行和regexp匹配的Benchmark
  • -benchtime t运行足够的次数,直到运行时间达到t
  • -count n每个test、benchmark、fuzz重复运行n次
  • -cpu 1,2,4 对每个test、benchmark、fuzz指定一个GOMAXPROCS值的列表,每个测试会以列表中的每个GOMAXPROCS值各执行一次。
  • -benchmem打印内存分配统计信息

slice

slice本质是数组片段的一个描述,包括数组指针、片段长度、容量。

切片操作不会复制切片指向的元素

创建一个新的切片会复用原来切片的底层数组

  • 尽可能在使用make()初始化slice时提供容量信息,避免过多次数的内存分配

    make([]T, len, cap)

    向slice中添加元素时,若slice容量不够,会创建一个更大的新的底层数组,将原切片中的元素复制过去。预先分配容量可减少重新分配内存的次数

  • 使用copy代替re-slice

    在一个切片的基础上创建切片,不会重新创建底层数组,如果原切片较大,新切片较小,小切片会始原数组在内存中有引用,得不到释放。

Map

  • map预分配内存

    make(map[K]V, size)

    可减少内存拷贝和rehash的消耗

字符串处理

  • 使用strings.Builder拼接字符串

  • 使用bytes.Buffer拼接字符串

    字符串是不可变类型,使用+拼接每次会重新分配内存。strings.Builder、bytes.Buffer底层都是[]byte数组,按一定的策略扩容,不需要每次拼接都重新分配内存。

  • 将bytes.Buffer转化为字符串时会重新申请一块内存,而strings.Builder直接将底层的[]byte转化成字符串类型返回,性能要好一些。
  • 如果已知最终字符串长度,可builder.Grow(len)buff.Grow(len),一次性分配内存

空结构体

  • 使用空结构体节省内存

空结构体struct{}实例不占用任何内存,可作为一些场景下的占位符

用map实现set,用空结构体作为值

atomic包

多个线程需要使用同一个变量时

  • 加锁

     type mutexCounter struct {
       i int32
       m sync.Mutex
     }
     ​
     func MutexAddOne(c *mutexCounter) {
       c.m.Lock()
       c.i++
       c.m.Unlock()
     }
    
  • 使用atomic包

     type atomicConter struct {
       i int32
     }
     ​
     func AtomicAddOne(c *atomicCounter) {
       atomic.AddInt32(&c.i, 1)
     }
    

    锁的实现是通过操作系统来实现,属于系统调用,atomic包操作是通过硬件实现,效率比锁高。

    锁应该用来保护一段逻辑,不仅仅用于保护一个变量。

    对于非数值操作,可以使用atomic.Value,能承载一个interface{}