Go 优化之路
来自 GOCN.VIP 的 2020 深圳 gopher meet up,演讲者陈一枭, GitHub ,观后笔记。
前言
优化是有成本的,需要权衡优化成本和优化价值。而且随着优化的推进,收益递减,所以我们必须知道 何时停止,并且 目标明确。
比如:优化目标是改进CPU,那么什么是可接受的速度,你想要将当前的性能提高多少?2倍还是10倍?如果目标是减少内存使用量,那么我们可以接受的速度有多慢,换句话说,你打算用多少速度去换取较低的需求。
停止优化的信号是什么?例如优化网络I/O服务,当我们发现Read/Write
操作均消耗在syscall.Syscall
则无需继续优化。
How to
在尝试改善一段代码性能前,我们需要先了解 当前性能。
优化是一种重构形式,我们以提高性能为目标,那么就意味这要以代码可读性为代价。
既然是重构,为了确保没有破坏任何内容,所以我们需要一套全面的单元测试,以及一套很好的基准测试,用来记录你的改变对性能产生的影响。
基准测试
Go 的标准库 testing
,提供了 benchmark
功能,通过执行 go test -bench=.
来进行基准测试。
分析
关注不同部分的优化影响,如果将仅占 5%
的例程速度提高一倍,那么整个项目的速度只提高了 2.5%
,相反,你将占 80%
的部分加速10%
,那么整个项目将提高 8%
。
使用工具: GODEBUG,go tool pprof,go tool trace
校验
使用 benchstat 来统计测试,
先安装 benchstat:
go get golang.org/x/pref/cmd/benchstat
这里举个例子,看以下代码:
func MyItoa(i int) string {
return fmt.Sprint(i) //版本一
//return strconv.Itoa(i) //版本二
}
var r string
func BenchmarkMyItoa(b *testing.B) {
for i := 0; i < b.N; i++ {
r = MyItoa(i)
}
}
分别对版本一和版本二执行:
go test -bench=. -count=10 > ver1.txt
go test -bench=. -count=10 > ver1.txt
然后进行对比:
benchstat ver1.txt ver2.txt
name old time/op new time/op delta
MyItoa-4 148ns ± 2% 47ns ± 2% -68.40% (p=0.000 n=10+9)
可以看到,性能损耗降低了68%。
实践
使用 syncPool复用对象,复用已分配对象,减少分配数量,降低GC压力,注意重置复用对象,避免取到脏数据。
使用成员变量复用对象。
写时复制代替互斥锁。
分区:减少加锁粒度
避免包含指针结构体作为map的key,因为在GC时,运行时扫描包含指针对象并且进行追踪。解决:在插入map之前将字符串散列为整数(以下为例)。
优化前
func timeGC() {
t := time.Now()
runtime.GC()
fmt.Printf("gc took: %s
", time.Since(t))
}
// go中string是指针
var pointers = map[string]int{}
func main() {
for i := 0; i < 10000000; i++ {
pointers[strconv.Itoa(i)] = i
}
for {
timeGC()
time.Sleep(1 * time.Second)
}
}
优化为:
type Entity struct {
A int
B float64
}
var entities = map[Entity]int{}
func main() {
for i := 0; i < 10000000; i++ {
entities[Entity{
A: i,
B: float64(i),
}] = i
}
for {
timeGC()
time.Sleep(1 * time.Second)
}
}
使用 strings.Builder
拼接字符串
避免[]byte
和 string
的转换
多读少写,使用 sync.RWMutex
代替 sync.Mutex