这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
一、本堂课重点内容
- bench 使用
- 常用性能优化策略
- pprof 使用
二、详细知识点介绍:
bench 使用
-
查看参数说明
go help testflag -
给出内存分配, 大小, 次数等统计
go test -bench=. -benchmem
预分配内存
slice, map 注意预分配内存, 能有效减少内存分配次数
陷阱: 大内存未释放
-
在已有切片基础上创建切片,不会创建新的底层数组
-
场景
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有引用,得不到释放
-
可使用 copy 替代re-slice
func GetLastBySlice(origin [lint) [lint { return origin[len(origin)-2:] } func GetLastByCopy(origin [lint) [lint { result := make( [lint, 2) copy(result, origin[len(origin)-2:]) return result }
字符串处理
+- strings.Builder
- bytes.Buffer
使用 + 拼接性能最差,strings.Builder, bytes.Buffer 相近,strings.Buffer 更快
- 宇符串在 Go 语言中是不可变类型,占用内存大小是固定的
- 使用
+每次都会重新分配内存 strings.Builder,bytes.Buffer底层都是 byte 数组- 内存扩容策略,不需要每次拼接重新分配内存
为什么 strings.Buffer 更快?
bytes.Buffer 转化为字符串时重新申请了一块空间
// To build strings more efficiently, see the strings. Builder type.
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
strings.Builder 直接将底层的 Byte 转换成了字符串类型返回
// String returns the accumulated string.
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
巧用空结构体
节省内存
- 空结构体实例不占据任何的内存空间
- 可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; it+ {
m[i] = struct{k}
}
}
func BoolMap(n int) {
m := make(map[int]bool)
for i := 0; i < n; it+ {
m[i] = false
}
}
实现 Set
- 实现 Set,可以考虑用 map 来代替
- 对于这个场景,只需要用到 map 的键,而不需要值
- 即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间
使用 atomic 代替加锁
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(Sc.I, 1)
}
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic 操作是通过硬件实现,效率比锁高
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}
三、实践练习例子:
是用 pprof 进行性能问题排查
启动 http 服务
以下面的代码为例
package main
import (
"log"
"net/http"
// 会自动注册 handler 到 http server,方便通过 http 接口获取程序运行采样
_ "net/http/pprof"
"os"
"runtime"
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)
runtime.GOMAXPROCS(1) // 限制 CPU 使用数,避免过载
runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪
runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪
// 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了
if err := http.ListenAndServe(":9091", nil); err != nil {
log.Fatal(err)
}
os.Exit(0)
}
项目启动后就可以测试了, 访问 http://localhost:9091/debug/pprof/
| 指标 | 内容 |
|---|---|
| allocs | 内存分配情况的采样信息 |
| blocks | 阻塞操作情况的采样信息 |
| cmdline | 显示程序启动命令及参数 |
| goroutine | 当前所有协程的堆栈信息 |
| heap | 堆上内存使用情况的采样信息 |
| mutex | 锁争用情况的采样信息 |
| profile | CPU 占用情况的采样信息 |
| threadcreate | 系统线程创建情况的采样信息 |
| trace | 程序运行跟踪信息 |
trace 可以行参阅《深入浅出 Go trace》
使用 pprof 命令行
注意, 对于不同的指标, 连接不一样
svg 或者 top 并不一定能完全找到问题, 可能 http 被阻塞, 注意查看网页里的原始数据
CPU
go tool pprof http://localhost:6060/debug/pprof/profile
内存
go tool pprof http://localhost:6060/debug/pprof/heap
GC
排查频繁内存回收
频繁的 GC 对 golang 程序性能的影响也是非常严重的。内存使用量并不高,可能 GC 过于频繁.
为了获取程序运行过程中 GC 日志,我们需要先退出程序,再在重新启动前赋予一个环境变量,同时为了避免其他日志的干扰,使用 grep 筛选出 GC 日志查看:
GODEBUG=gctrace=1 ./go-pprof-practice | grep gc
注意 GC 的间隔时间
接下来使用 pprof 排查时,关注的不是什么地方在占用大量内存,而是什么地方在不停地申请内存。
go tool pprof http://localhost:6060/debug/pprof/allocs
由于内存的申请与释放频度是需要一段时间来统计的,需要保证程序已经运行了几分钟之后,再运行命令
在 golang 里,对象是使用堆内存还是栈内存,由编译器进行逃逸分析并决定,如果对象不会逃逸,便可在使用栈内存,但总有意外,就是对象的尺寸过大时,便不得不使用堆内存。所以这里设置申请 16 MiB 的内存就是为了避免编译器直接在栈上分配,如果那样得话就不会涉及到 GC 了。
Goroutine
在 golang 中,协程本身是可能泄露的,或者叫协程失控,进而导致内存泄露。
go tool pprof http://localhost:6060/debug/pprof/goroutine
锁
go tool pprof http://localhost:6060/debug/pprof/mutex
阻塞 (block)
go tool pprof http://localhost:6060/debug/pprof/block
常用内置命令
top
list
web
功能是生成 svg 图片, 需要提前安装 graphviz
四、课后个人总结:
pprof 大法好
五、引用参考:
- pprof 实战 github.com/wolfogre/go…