Go 性能优化

常见的优化手段

  • 池化技术。比如 sync.Pool 对象池(频繁分配同一类型的多个对象);协程池(Jeffail/tunny, panjf2000/ants);连接池(buraksezer/connpool);内存池等

  • 复用对象。比如 string2bytes

  • 优化反射。https://github.com/goccy/go-reflect

  • 减小锁消耗。减小锁粒度;使用原子操作(atomic)代替互斥锁。读多写少的场景使用读写锁(RWMutex)

  • 减小锁竞争。比如 go-cache/bigcache 等缓存库都是通过分片功能减少锁竞争

优化建议:

  • 预分配内存。slice/map 如果预知容量信息,初始化应该提供,减少内存重分配和复制元素的消耗。 make([]int, 0, cap); make(map[int]int, cap)

  • 使用 strings.Builder 拼接大量字符串

  • 使用空的结构体作为占位符,空结构体 struct{} 不占内存。 比如实现 set 使用 map[string]struct{}{}

  • 使用 atomic 代替 sync 包。atomic 包的操作指令级支持

  • struct 合理的布局可以减少内存占用,提升性能(内存对齐)。一个简单的规则是按照字节大小倒序排列。

  • 尽量减少逃逸,将变量限制在栈上,减少堆变量分配,降低 GC 成本,提高程序性能

    • 局部切片尽可能确定长度或者容量。长度和容量尽可能是一个常量,如果是一个变量go无法判断其大小会逃逸到堆上分配

    • 函数返回指针可以减少拷贝但是会导致内存分配逃逸到堆中。如果要修改原对象或者返回内存比较大的对象,返回指针。对于只读或 者占用内存小的对象,可以直接返回值。

    • 如果返回值的类型可以确定,就不要用 interface{}

string 与 []byte 互转

利用了底层 string 和 byte slice 实现的技巧,如果需要大量互转可以使用这种方式。

/*
type StringHeader struct { // reflect.StringHeader
    Data uintptr
    Len  int
}
type SliceHeader struct { // reflect.SliceHeader
    Data uintptr
    Len  int
    Cap  int
}
*/

// NOTE:注意之后不要修改 string
func str2bytes(s string) []byte {
   x := (*[2]uintptr)(unsafe.Pointer(&s))
   b := [3]uintptr{x[0], x[1], x[1]}
   return *(*[]byte)(unsafe.Pointer(&b))
}

func bytes2str(b []byte) string {
   return *(*string)(unsafe.Pointer(&b))
}

大量字符串拼接

字符串在 go 中是不可变对象。大量字符串(一般超过 5 个字符串)拼接不要用 + ,使用 bytes.Buffer 或者 strings.Builder。 不过对于个数比较少的字符串拼接,直接用 + 效率也很高,比 fmt.Sprintf 更快,所以少量字符串拼接可以放心使用 + 。

// bytes.Buffer
package main

import (
    "fmt"
    "bytes"
)

func main() {
    var b bytes.Buffer

    b.WriteString("abc")
    b.WriteString("def")

    fmt.Println(b.String()) // abcdef
}
// strings.Builder
package main

import (
    "fmt"
    "strings"
)

func main() {
    var sb strings.Builder
    sb.WriteString("First")
    sb.WriteString("Second")
    fmt.Println(sb.String())    // FirstSecond
}

更快的随机数

Go 内置的 rand.Int()在生成随机数时,为了并发安全底层使用了锁,在高并发常见下会有性能问题。 可以使用 github.com/valyala/fastrand 等三方库替换。

伪共享问题(false sharing)

如果并发更新一个结构体的字段,我们可以通过填充空字节防止字段被 cpu 缓存到一个 cache line 单位中,需要不断同步降低效率。 可以在 https://github.com/uber-go/ratelimit 中找到一个例子:

type leakyBucketLimiter struct {
    state unsafe.Pointer // 是一个状态的指针,用于存储上一次的执行的时间,以及需要 sleep  的时间

    //lint:ignore U1000 Padding is unused but it is crucial to maintain performance
    // of this rate limiter in case of collocation with other frequently accessed memory.
    padding [56]byte // cache line size - state pointer size = 64 - 8; created to avoid false sharing.(伪共享)
    // cpu cache 一般是以 cache line 为单位的,在 64 位的机器上一般是 64 字节
    // 所以如果我们高频并发访问的数据小于 64 字节的时候就可能会和其他数据一起缓存,其他数据如果出现改变就会导致 cpu 认为缓存失效,这就是 false sharing
    // 所以在这里为了尽可能提高性能,填充了 56 字节的无意义数据,因为 state 是一个指针占用了 8 个字节,所以 64 - 8 = 56

    perRequest time.Duration // perRequest = 1s / rate,每个请求间隔 1s/perRequest
    maxSlack   time.Duration // 松弛时间,也就是可以允许的突发流量的大小,默认是 Pre / 10
}

正确设置容器 CPU 配额

容器中运行 Go 程序需要正确设置 GOMAXPROCS,推荐使用 https://github.com/uber-go/automaxprocs 这个库,直接一行代码就可以。 import _ "go.uber.org/automaxprocs"