性能优化指南 | 豆包MarsCode AI刷题

212 阅读4分钟

性能优化指南

  • 简介
    • 性能优化的前提是满足正确可靠、简介清晰等质量因素
    • 性能有话是综合评估,有时候时间效率和空间效率可能对立
    • 针对 Go 语言特性,介绍 Go 相关的性能优化建议

1 Benchmark

  • 如何使用
    • 性能表现需要实际数量衡量
    • Go 语言提供了支持基准性能测试的 benchmark 工具
  • 补充
    • import "testing"
    • 在终端目录文件夹下输入命令 go mod init + 模块名称 用来初始化 go.mod 文件,否则会无法使用 benckhmark 工具 00.png
    • 使用时可直接在命令行输入 go test -bench. -benchmem 如下 01.png .png)
    • 或者在 vscode 中点击 run benchmark 02.png
    • 都可得到如下结果 03.png
      • BenchmarkPreAlloc 是测试的函数名
      • -20 表示GOMAXPROCS(CPU核数)的值为20
      • 54102753 表示一共执行54102753次 即b.N的值
      • 22.48 ns/op 每次执行花费22.48ns
      • 80 B/op 每次执行申请多大的内存
      • 1 allocs/op 每次执行申请几次内存

2 Slice

  • slice 预分配内存
    • 尽可能在使用 make() 初始化切片时提供容量信息
    • 切片本质是一个数组片段的描述
      • 包括数组指针
      • 片段的长度
      • 片段的容量(不改变内存分配情况下的最大长度)
      type slice struct{
          array unsafe.Pointer
          len int
          cap int
      }
      
    • 切片操作并不复制切片指向的元素
    • 创建一个新的切片会复用原来切片的底层数组
    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)
        }
    }
    
    04.png

3 Map

  • map 预内存分配
    • 不断向 map 中添加元素的操作会触发 map 的扩容
    • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
    • 建议根据实际需求提前预估好需要的空间
    func NoPreAlloc(size int) {
        data := make(map[int]int)
        for k := 0; k < size; k++ {
            data[k] = 1
        }
    }
    
    func PreAlloc(size int) {
        data := make(map[int]int, size)
        for k := 0; k < size; k++ {
            data[k] = 1
        }
    }
    
    05.png

4 String

  • 使用 strings.Builder
    • 常见的字符串拼接方式
    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()
    }
    
    func PreStrBuilder(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 PreByteBuffer(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()
    }
    
    07.png
    • 使用 + 拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer 更快
    • 字符串在 G0 语言中是不可变类型,占用内存大小是固定的
    • 使用 + 每次都会重新分配内存
    • strings.Builder,bytes.Buffer 底层都是 []byte 数组
    • 内存扩容策略,不需要每次拼接重新分配内存
    • bytes.Buffer 转化为字符串时重新申请了一块空间
    • strings.Builder 直接将底层的 []byte 转换成了字符串类型返回

5 空结构体

  • 使用空结构体节省内存
    • 空结构体 struct{} 实例不占据任何的内存空间
    • 可作为各种场景下的占位符使用
      • 节省资源
      • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
    func EmptyStructMap(n int) {
        m := make(map[int]struct{})
        for i := 0; i < n; i++ {
            m[i] = struct{}{}
        }
    }
    
    func BoolMap(n int) {
        m := make(map[int]bool)
        for i := 0; i < n; i++ {
            m[i] = false
        }
    }
    
    08.png
    • 实现 Set,可以考虑用 map 来代替
    • 对于这个场景,只需要用到 map 的键,而不需要值
    • 即使是将 map 的值设置为 bool 类型,也会多占据1个字节空间

一个开源实现:github.com/deckarep/go…

6 atomic 包

  • 如何使用 atomic 包
    type atomicCounter struct {
        i int32
    }
    
    func AtomicAddOne(c *atomicCounter) {
        atomic.AddInt32(&c.i, 1)
    }
    
    type mutexCounter struct {
        i int32
        m sync.Mutex
    }
    
    func MutexAddOne(c *mutexCounter) {
        c.m.Lock()
        c.i++
        c.m.Unlock()
    }
    
    10.png
    • 锁的实现是通过操作系统来实现,属于系统调用
    • atomic 操作是通过硬件实现,效率比锁高
    • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
    • 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}

小结

  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 普通应用代码,不要一味地追求程序的性能
  • 越高级的性能优化手段越容易出现问题
  • 在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能