编写高性能的 Go 程序~
前言:
继上次课程的高质量编程内容讲解,本次课程主要介绍了在满足正确性、可靠性、健壮性、可读性等质量因素的前提下提高程序效率的性能优化建议;性能优化分析工具;以及性能调优的实战案例,分享了业务优化、基础库优化和 Go 语言优化的流程和方式 …
性能优化建议
Go 语言提供了支持基准性能测试的 Benchmark 工具,指令如下:
go test -bench=. -benchmem
💻对下面的代码执行测试指令:
// from fib.go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n - 1) + Fib(n - 2)
}
// from fib_test.go
func BenchmarkFib10(b *testing.B) {
// run the Fib function b.N times
for n := 0; n < b.N; n++ {
Fib(10)
}
}
- PS:使用 Benchmark 工具对优化前后的代码进行测试,评估代码性能。
性能优化的前提是满足正确可靠、简洁清晰等质量因素,性能优化是综合评估,有时候实践效率和空间效率可能对立,下面针对 Go 语言特性,给出了 Go 相关的性能优化建议。
优化建议 - Slice:
切片的本质是一个数组片段的描述,包含以下三项:
- 数组指针
array unsafe.Pointer
- 片段的长度
len
- 片段的容量
cap
(不改变内存分配情况下的最大长度)
尽可能在使用 make()
初始化切片时提供容量信息。这是因为向切片中添加的元素数量超过默认容量会触发扩容机制,扩容是一个比较耗时的操作。
func PreAlloc(size int) {
data := make([]int, 0, size)
for k:= 0; k < size; k++ {
data = append(data, k)
}
}
🎈切片使用陷阱:大内存未释放
- 场景:
- 原切片较大,代码在原切片基础上新建小切片。
- 原底层数组在内存中有引用,得不到释放。
这是由于 Golang 中在已有切片的基础上创建切片,不会创建新的底层数组,而是直接复用原来的。如果只是需要用到其中的一小部分,复用原来的整个数组会导致占用较大的内存空间,建议使用 copy
替代 re-slice。
// re-slice,占用空间较大:
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
// copy,占用空间小,推荐使用:
func GetLastBySlice(origin []int) []int {
result := make([]int, 2]
copy(result, origin[len(origin)-2:])
return result
}
优化建议 - Map:
同样的,map 也建议预分配内存来避免扩容机制的时间开销。
- 不断向 map 中添加元素会触发 map 的扩容。
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗。
- 建议根据实际需求提前预估好需要的空间。
func GetLastBySlice(origin []int) []int {
data := make(map[int]int, size)
for i := 0; i < size; i++ {
data[i] = 666
}
}
优化建议 - 字符串处理:
和 Java 语言类似,Golang 中直接使用 +
拼接字符串是一种十分低效的方式,因为字符串是不可变类型,使用 +
每次都会重新分配内存,推荐使用 strings.Builder
或 bytes.Buffer
操作字符串(strings.Builder
效率要更高一些)。
// 使用加号拼接字符串,不推荐
func Plus(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s += str
}
return s
}
// 使用strings.Builder拼接字符串
func StrBuilder(n int, str string) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}
此外 strings.Builder
和 bytes.Buffer
都支持使用 Grow()
函数预分配内存,在可以预知长度的情况下提前分配内存,可以提高字符串拼接的效率。
// strings.Builder:
var builder strings.Builder
builder.Grow(n * len(str))
// bytes.Buffer:
buf := new(bytes.Buffer)
buf.Grow(n * len(str))
优化建议 - 空结构体:
使用空结构体 struct{}
可以节省内存。
- 空结构体实例不占据任何的内存空间。
- 可作为各种场景下的占位符使用。
- 节省资源。
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符。
比如在实际的开发中,我们经常会使用到 Set 这种数据结构,然而 Golang 本身并不支持 Set,我们可以考虑用 map 来代替。换句话说我们只用到 map 的键,而不用它的值,那么值可以用 struct{}
类型占位。
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
}
优化建议 - atomic 包:
atomic 包主要用在多线程编程,相比于加锁的方式来保证并发安全,atomic 包效率更高。
- 锁的实现是通过操作系统来实现,属于系统调用。
- atomic 操作是通过硬件实现,效率比锁高。
sync.Mutex
应该用来保护一段逻辑,不仅仅用于保护一个变量,因此成本比较大。- 对于非数值操作,可以使用
atomic.Value
,能承载一个interface{}
。
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
性能优化分析工具
pprof 是用于可视化和分析性能、分析数据的工具,帮助我们了解应用在什么地方耗费了多少 CPU、内存等。
pprof - 排查实战
GitHub 上提供了 pprof 工具的实验项目,通过对项目的性能分析实战,帮助我们了解 pprof 工具的使用流程,便于我们今后分析一些更为复杂的程序。
🎈前置准备:
下载 GitHub 上的项目代码,该项目提前买入了一些炸弹代码,产生可观测的性能问题。
🚀项目传送门: wolfogre/go-pprof-practice: go pprof practice. (github.com)
🚀实战手册传送门: golang pprof 实战 | Wolfogre’s Blog
🎈pprof 命令总结:
pprof 实战手册中给出了详细的项目实验步骤,这里就不再赘述了,主要记录以下课程中主讲老师给出的一些 pprof 的常用命令。
- 启动项目以后可以使用浏览器访问
localhost:6060/debug/pprof
查看指标,在项目的main.go
文件中指定了 pprof 的访问端口:// 代码片段... go func() { // 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了 if err := http.ListenAndServe(":6060", nil); err != nil { log.Fatal(err) } os.Exit(0) }() // ...
- pprof 提供了命令来在终端中获取采样数据:
随后可以使用接下来的各种命令来有针对性的获取特定资源的使用情况。go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
topN
命令查看占用资源最多的函数,会显示以下几点数据:- flat - 当前函数本身的执行耗时;
- flat% - flat 占 CPU 总时间的比例;
- sum% - 上面每一行的 flat% 总和;
- cum - 指当前函数本身加上其调用函数的总耗时;
- cum% - cum 占 CPU 总时间的比例。
list
命令用来根据指定的正则表达式查找代码行。web
命令用来将调用关系可视化展示。- pprof 的浏览器面板会将所有指标以平铺的方式展现,看起来并不直观,在
pprof
命令中加上一个可选项可以以可视化的方式展现监控数据。- 查看堆内存:
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
- 查看 goroutine:
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
- 查看堆内存:
性能调优
本次课程性能调优部分主通过实际业务服务性能优化的案例介绍了性能调优的思路,可以从三方面入手:业务服务优化、基础库优化、Go 语言优化。
业务服务优化
🎯基本概念:
- 服务:能单独部署。能承载一定功能的程序。
- 依赖:Service A 的功能实现依赖 Service B 的响应结果,称 Service A 依赖 Service B。
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系。
- 基础库:公共的工具包、中间件。
☕流程:
- 建立服务性能评估手段。
- 分析性能数据,定位性能瓶颈。
- 重点优化项改造。
- 优化效果验证。
基础库优化
☕流程:
- 分析基础库核心逻辑和性能瓶颈。
- 完善设计改造方案;
- 数据按需获取;
- 数据序列化协议优化。
- 内部测压验证。
- 推广业务服务落地验证。
Go 语言优化
🍬编译器 & 运行时优化:
- 优化内存分配策略。
- 优化代码编译流程,生成更高效的程序。
- 内部测压验证。
- 推广业务服务落地验证。