前言:
继上次课程的高质量编程内容讲解,本次课程主要介绍了在满足正确性、可靠性、健壮性、可读性等质量因素的前提下提高程序效率的性能优化建议;性能优化分析工具;以及性能调优的实战案例,分享了业务优化、基础库优化和 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)
}
}
性能优化的前提是满足正确可靠、简洁清晰等质量因素,性能优化是综合评估,有时候实践效率和空间效率可能对立,下面针对 Go 语言特性,给出了 Go 相关的性能优化建议。
切片的本质是一个数组片段的描述,包含以下三项:
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 也建议预分配内存来避免扩容机制的时间开销。
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 包效率更高。
sync.Mutex
应该用来保护一段逻辑,不仅仅用于保护一个变量,因此成本比较大。atomic.Value
,能承载一个 interface{}
。type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
pprof 是用于可视化和分析性能、分析数据的工具,帮助我们了解应用在什么地方耗费了多少 CPU、内存等。
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)
}()
// ...
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
随后可以使用接下来的各种命令来有针对性的获取特定资源的使用情况。topN
命令查看占用资源最多的函数,会显示以下几点数据:
list
命令用来根据指定的正则表达式查找代码行。web
命令用来将调用关系可视化展示。pprof
命令中加上一个可选项可以以可视化的方式展现监控数据。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
本次课程性能调优部分主通过实际业务服务性能优化的案例介绍了性能调优的思路,可以从三方面入手:业务服务优化、基础库优化、Go 语言优化。
基本概念:
☕流程:
☕流程:
编译器 & 运行时优化: