[性能调优 (一) | 青训营笔记]

71 阅读3分钟

讲解Go写代码时性能如何优化

我们写代码的时候可能无意间就写出来了性能较差的代码,所以我们要学会性能调优,通过手段去把我们的程序进行优化,主要有Slice优化,Map优化,字符串处理优化......

1、Slice

大内存未释放

  • 在已有切片上创建切片,不会创建新的底层数组

  • 场景:

    • 原切片较大,代码在原切片的基础上创建新的小切片
    • 原底层数组在内存中有引用,得不到释放
  • 可以用copy代替re-slice

 func GetLastBySlice(origin []int) []int {
     return origin[len(origin)-2:]
 }
 ​
 func GetLastCopy(origin []int) []int {
     result := make([]int, 2)
     copy(result, origin[len(origin) - 2:])
     return result
 }
 ​
 func testGetLast(t *testing.T, f func([]int) []int) {
     result := make([]int, 0)
 ​
     for i := 0; i < 100; i ++ {
         origin := make([]int, 128 * 1024)
         result = append(result, f(origin)...)
     }
 }
 ​
 func TestLastBySlice(t *testing.T) {
     testGetLast(t, GetLastBySlice)
 }
 ​
 func TestLastByCopy(t *testing.T) {
     testGetLast(t, GetLastCopy)
 }

image-20230519145106553

可见我们使用copy的时候内存和时间花销小很多

2、map优化

map使用时也要预先分配内存

3、字符串处理

我们常用的字符串处理

1.直接拼接

 func plus(n int, str string) {
     s := " "
     for i := 0; i < n; i ++ {
         s += str
     }
 }

2.使用strings.Builder()

 func StrBuilder(n int, str string) {
     var buf strings.Builder
     for i := 0; i < n; i++ {
         buf.WriteString(str)
     }
 }

3.使用ByteBuffer()

 func ByteBuffer(n int, str string) {
     buf := new(bytes.Buffer)
 ​
     for i := 0; i < n; i++ {
         buf.WriteString(str)
     }
 }

测试结果

 # 测试Plus直接加
 go test -bench=Plus
 goos: windows
 goarch: amd64
 pkg: goTest/test
 cpu: AMD Ryzen 5 4600U with Radeon Graphics
 BenchmarkPlus-12          296730            109249 ns/op
 PASS
 ok      goTest/test     32.560s
 ​
 #每次耗时109249 ns/op
 ​
 # 测试StrBuilder
 go test -bench=StrBuilder
 goos: windows
 goarch: amd64
 pkg: goTest/test
 cpu: AMD Ryzen 5 4600U with Radeon Graphics
 BenchmarkStrBuilder-12            231668             83853 ns/op
 PASS
 ok      goTest/test     20.143s
 ​
 # 每次耗时83853 ns/op
 ​
 # 测试ByteBuffer
 go test -bench=ByteBuffer
 goos: windows
 goarch: amd64
 pkg: goTest/test
 cpu: AMD Ryzen 5 4600U with Radeon Graphics
 BenchmarkByteBuffer-12            257964             95089 ns/op
 PASS
 ok      goTest/test     24.682s
 ​
 # 每次耗时95089 ns/op
 ​
 最快的是strings.Builder()
 所以我们字符串拼接的时候用strings.Builder()是比较快的

原因分析

因为字符串在Go中是不可变类型,

  • 占用内存大小固定,每次使用+都会重新分配内存
  • 另外两种方法底层都是[]byte数组
  • 内存扩容策略,不需要重新分配内存

4、atomic包

这个包用来去维持一个原子操作

 type atomicCounter struct {
     i int32
 }
 //  atomic包
 func AtomicAddOne(c *atomicCounter) {
     atomic.AddInt32(&c.i, 1)
 }
 ​
 // 使用mutex进行操作
 type mutexCounter struct {
     i  int
     mu sync.Mutex
 }
 ​
 func MutexAddOne(c *mutexCounter) {
     c.mu.Lock()
     defer c.mu.Unlock()
     c.i++
 }
 BenchmarkAtomicAddOne-12        269700860            4.463 ns/op           0 B/op          0 allocs/op
 BenchmarkMutexAddOne-12         76684665            14.65 ns/op             0 B/op         0 allocs/op

从此处可见使用atomic包效率较高

原因

  • 锁的操作使用过操作系统实现的,属于系统调用
  • atomic包通过硬件实现,效率比锁高
  • 加锁应该保护一段逻辑,不仅仅保护一段变量
  • 非数值操作,可以使用atomic.Value,承载一个interface{}