性能优化建议
简介
性能优化的前提是满足正确可靠、简洁清晰等质量因素
性能优化是综合评估,有时候时间效率和空间效率可能对立
针对Go语言特性,介绍Go相关的性能优化建议
性能优化建议-Benchmark工具
如何使用
性能表现需要实际数据衡量
Go语言提供了支持基准性能测试的benchmark工具
示例代码
fib.go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
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 test -benchmem -run=^
GOMAXPROCS 1.5版本后,默认值为CPU核数pkg.go.dev/runtime#GOM…
性能优化建议-Slice
slice预分配内存
尽可能在使用make()初始化切片时提供容量信息
示例代码
// 不使用预分配内存
func NoPreAlloc(size int) {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
// 使用预分配内存
func PreAlloc(size int) {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
测试文件
package main
import (
"testing"
)
func BenchmarkNoPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
NoPreAlloc(10)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
PreAlloc(10)
}
}
func BenchmarkFunctions(b *testing.B) {
b.Run("NoPreAlloc", BenchmarkNoPreAlloc)
b.Run("PreAlloc", BenchmarkPreAlloc)
}
运行结果
运行指令:go test -benchmem -run=^
结果中可以看出,使用预分配内存可以有效地提高程序性能
切片本质是一个数组片段的描述
包括数组指针
片段的长度
片段的容量(不改变内存分配情况下的最大长度)
切片操作并不复制切片指向的元素
创建一个新的切片会复用原来切片的底层数组
另一个陷阱:大内存未释放
在已有切片基础上创建切片,不会创建新的底层数组
场景
原切片较大,代码在原切片基础上新建小切片
原底层数组在内存中有引用,得不到释放
可使用copy代替re-slice
示例代码
func GetLastBySlice(origin []int) []int {
return origin[len(origin)-2:]
}
func GetLastByCopy(origin []int) []int {
result := make([]int, 2)
copy(result, origin[len(origin)-2:])
return result
}
func testGetLast(t *testing.T, f func([]int) []int) {
result := make([][]int, 0)
for k := 0; k < 100; k++ {
origin := generateWithCap(128 * 1024)
result = append(result, f(origin))
}
printMen(t)
_ = result
}
运行结果
性能优化建议-Map
map预分配内存
示例代码
func NoPreAlloc(size int) {
// map没有预分配内存
data := make(map[int]int)
for i := 0; i < size; i++ {
data[i] = 1
}
}
func PreAlloc(size int) {
// map有预分配内存
data := make(map[int]int, size)
for i := 0; i < size; i++ {
data[i] = 1
}
}
测试文件
package main
import (
"testing"
)
func BenchmarkNoPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
NoPreAlloc(10)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
PreAlloc(10)
}
}
func BenchmarkFunctions(b *testing.B) {
b.Run("NoPreAlloc", BenchmarkNoPreAlloc)
b.Run("PreAlloc", BenchmarkPreAlloc)
}
运行结果
运行指令:go test -benchmem -run=^
结果分析
不断向map中添加元素的操作会触发map的扩容
提前分配好空间可以减少内存拷贝和Rehash的消耗
建议根据实际需求提前预估好需要的空间
性能优化建议-字符串处理
使用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()
}
// 使用bytes.Buffer
func ByteBuffer(n int, str string) string {
buf := new(bytes.Buffer)
for i := 0; i < n; i++ {
buf.WriteString(str)
}
return buf.String()
}
测试文件
func BenchmarkPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
Plus(20, "10")
}
}
func BenchmarkStrBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
StrBuilder(20, "10")
}
}
func BenchmarkByteBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
ByteBuffer(20, "10")
}
}
func BenchmarkFunctions02(b *testing.B) {
b.Run("Plus", BenchmarkPlus)
b.Run("StrBuilder", BenchmarkStrBuilder)
b.Run("ByteBuffer", BenchmarkByteBuffer)
}
运行结果
运行指令:go test -benchmem -run=^
使用“+”号拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer更快
分析
字符串在Go语言中是不可变类型,占用内存大小是固定的
使用+每次都会重新分配内存
strings.Builder,bytes.Buffer底层都是[]byte数组
内存扩容策略,不需要每次拼接重新分配内存
bytes.Buffer转化为字符串时重新申请了一块空间
strings.Builder直接将底层的[]byte转换成了字符串类型返回
性能优化建议-空结构体
使用空结构体节省内存
空结构体struct{}实例不占据任何的内存空间
可作为各种场景下的占位符使用
节省资源
空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
示例代码
func EmptyStructMap(n int) {
m := make(map[int]struct{})
for i := 0; i < n; i++ {
m[i] = struct{}{}
}
}
func BoolMap(n int) {
m := make(map[int]bool)
for i := 0; i < n; i++ {
m[i] = false
}
}
运行结果
运行指令:go test -benchmem -run=^
实现Set,可以考虑用map来代替
对于这个场景,只需要用到map的键,而不需要值
即使是将map的值设置为bool类型,也会多占据1个字节空间
性能优化建议-atomic包
如何使用atomic包
// 使用atomic实现加1
type atomicCounter struct {
i int32
}
func AtomicAddOne(c *atomicCounter) {
atomic.AddInt32(&c.i, 1)
}
//使用锁实现加1
type mutexCounter struct {
i int32
m sync.Mutex
}
func MutexAddOne(c *mutexCounter) {
c.m.Lock()
c.i++
c.m.Unlock()
}
测试文件
func BenchmarkAtomicAddOne(b *testing.B) {
counter := &atomicCounter{}
b.ResetTimer() // 重置计时器
// 并发运行加1操作
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
AtomicAddOne(counter)
}
})
}
func BenchmarkMutexAddOne(b *testing.B) {
counter := &mutexCounter{}
b.ResetTimer() // 重置计时器
// 并发运行加1操作
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
MutexAddOne(counter)
}
})
}
func BenchmarkFunctions04(b *testing.B) {
b.Run("AtomicAddOne", BenchmarkAtomicAddOne)
b.Run("MutexAddOne", BenchmarkMutexAddOne)
}
运行结果
运行命令:go test -benchmem -run=^
使用atomic包
锁的实现是通过操作系统来实现,属于系统调用
atomic操作是通过硬件实现,效率比锁高
sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
对于非数值操作,可以使用atomic.Value,能承载一个interface{}
性能调优实战
性能调优原则
要依靠数据不是猜测
要定位最大瓶颈而不是细枝末节
不要过早优化
不要过度优化
性能分析工具pprof
说明
希望知道应用在什么地方耗费了多少CPU、Memory
pprof是用于可视化和分析性能分析数据的工具
功能简介
排查实战
搭建pprof实践项目
pprof实战项目源码:github.com/wolfogre/go…
项目提前埋入了一些炸弹代码,产生可观测的性能问题
下载源代码
编译项目
编译指令:go build
浏览器查看指标
./go-pprof-practice.exe运行程序
浏览器输入http://localhost:6060/debug/pprof/查看指标
CPU
命令行输入命令:go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"可以进入pprof对于这个项目的工具
查看占用资源最多的函数,命令top
flat 当前函数本身的执行耗时
flat% flat占CPU总时间的比例
sum% 上面每一行的flat%总和
cum 指当前函数本身加上其调用函数的总耗时
cum% cum占CPU总时间的比例
什么情况下flat==cum? 什么情况下flat==0
flat == cum, 函数中没有调用其他函数
flat == 0, 函数中只有其他函数的调用
根据指定的正则表达式查找代码行,命令list Eat
调用关系可视化,命令web
Heap-堆内存
解决完CPUbug后CPU和内存占用率
查看堆内存命令:go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
如果浏览器出现下面的内容,则需要安装graphviz软件,下载地址:graphviz.gitlab.io/
Top视图
Source视图
解决完bug后的内存占用
alloc_objects: 程序累计申请的对象数
alloc_space: 程序累计申请的内存大小
inuse_objects: 程序当前持有的对象数
inuse_space: 程序当前占用的内存大小
goroutine-协程
查看协程命令:go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
火焰图
由上到下表示调用顺序
每一块代表一个函数,越长代表占用CPU的时间越长
火焰图是动态的,支持点击快进行分析
支持搜索,在Source视图下搜索wolf
注释问题代码后的结果
mutex-锁
查看锁命令:go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
block-阻塞
查看block命令:go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"