Go语言性能调优及实战 | 青训营笔记

131 阅读3分钟

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

1 性能调优建议

如何评估代码性能:Go语言提供了支持基准性能测试的benchmark工具。

1.1 Slice

切片本质是一个数组片段的描述,包括数组指针、片段长度以及片段容量:

type slice struct {
    array unsafe.Pointer // 数组指针
    len   int            // 片段长度
    cap   int            // 片段容量
}

切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组。

  • slice预分配内存,切片容量足够时直接讲元素放入对应的内存,容量不足时会先进行一次扩容操作,因此尽可能在使用make初始化切片时提供容量信息;

  • 大内存释放问题,由于在已有切片的基础上创建切片时,不会创建新的底层数组,新的切片依然会引用原底层数组,当原切片较大时,就会造成内存的浪费,我们可以使用copy替代re-slice

    // re-slice
    func GetLastBySlice(origin []int) []int {
        return origin[len(origin)-2:]
    }
    ​
    // copy
    func GetLastByCopy(origin []int) []int {
        result := make([]int, 2)
        copy(result, origin[len(origin)-2:])
        return result
    }
    

1.2 Map

Map的性能调优方向和Slice类似,同样从预分配内存方向考虑:在不断向Map中添加元素时会触发扩容,提前分配好空间可以减少内存的拷贝以及Rehash的消耗,因此根据实际需求提前预估需要的空间是必要的。

1.3 String

使用strings.Builder或者bytes.Buffer进行字符串拼接的性能远远由于直接使用+进行拼接,性能表现上string.Buffer更快,其原因在于:

  • 字符串在Go语言中是不可变类型,因此占用的内存大小是固定的;

  • 使用+每次都会重新分配内存;

  • strings.Builderbytes.Buffer的底层都是[]byte数组,内存分配策略不需要每次拼接都重新分配内存;

  • bytes.Buffer在将[]byte数组转化为字符串时重新申请了一块空间,而strings.Builder是直接将[]byte数组转化为了字符串类型返回:

    // bytes.Buffer
    func (b *Buffer) String() string {
        if b == nil {
            return "<nil>"
        }
        return string(b.buf[b.off:])
    }
    // strings.Builder
    func (b *Builder) String() string {
        return *(*string)(unsafe.Pointer(&b.buf))
    }
    

在已知拼接的字符串长度的情况下,可以使用Grow(size)方法进一步提高字符串拼接的性能。

1.4 Struct

使用空结构体节省内存:空结构体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
    }
}

使用场景:

  • 实现Set可以考虑用Map代替,例如:golang-set
  • 只需要用到MapKEY,而不需要VALUE的场景;

1.5 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()
}

其原因在于:

  • 锁的实现是通过操作系统实现的,属于系统调用,而原子操作是通过硬件实现的,因此效率更高;
  • sync.Mutex用于保护一段逻辑,不仅仅保护一个变量;
  • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}

2 性能调优实战

性能调优原则:

  • 要依赖数据而不是猜测;
  • 要定位最大瓶颈而不是细枝末节;
  • 不要过早优化也不要过度优化;

2.1 性能分析工具pprof

  • 可以知道应用在什么地方耗费了多少CPU资源和内存资源;
  • 可以进行可视化的性能数据分析;

2.2 项目分析

项目地址

func main() {
    log.SetFlags(log.Lshortfile | log.LstdFlags)
    log.SetOutput(os.Stdout)
​
    runtime.GOMAXPROCS(1)              // 限制CPU使用数
    runtime.SetMutexProfileFraction(1) // 开启锁调用跟踪
    runtime.SetBlockProfileRate(1)     // 开启阻塞调用跟踪
​
    go func() {
        // 启动 HTTP server
        if err := http.ListenAndServe(":6060", nil); err != nil {
            log.Fatal(err)
        }
        os.Exit(0)
    }()
    // ...
}

访问:http://localhost:6060/debug/pprof/

image-20230119132349219.png

2.2.1 CPU性能排查

采集10秒的数据

go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"

image-20230119133055105.png

top命令

  • flat:当前函数本身的执行耗时
  • flat%:flat在CPU总时间的占比
  • sum%:上面每一行的flat%总和
  • cum:当前函数本身加上其调用函数的总耗时
  • cum%:cum在CPU总时间的占比

image-20230119133236255.png

当函数没有调用其它函数时,flat和cum相等;

当flat等于0时,函数只有其它函数的调用;

list命令

top命令的结果可知,Eat方法占用了很大的CPU资源,我们可以使用list Eat命令对Eat方法进行排查:

image-20230119134051789.png

2.2.2 Heap堆内存排查

go tool pprof "http://localhost:6060/debug/pprof/heap"

和CPU排查的方法类似:使用toplist进行问题定位

image-20230119134917510.png

2.2.3 协程排查

可以添加-http参数进行可视化分析:

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"

View中切换为Flame Graph,可以更直观地进行问题排查

image-20230119135852750.png

排查到wolf下的Drink方法存在问题:

func (w *Wolf) Drink() {
    log.Println(w.Name(), "drink")
    // 问题
    for i := 0; i < 10; i++ {
        go func() {
            time.Sleep(30 * time.Second)
        }()
    }
}

2.2.4 Mutex排查

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"

排查问题:

func (w *Wolf) Howl() {
    log.Println(w.Name(), "howl")
    // 问题
    m := &sync.Mutex{}
    m.Lock()
    go func() {
        time.Sleep(time.Second)
        m.Unlock()
    }()
    m.Lock()
}

2.2.5 阻塞排查

go tool pprof "http://localhost:6060/debug/pprof/block"

image-20230119141031985.png

pprof通过过滤策略会忽略一些点。