这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
day3 高质量编码规范与调优
昨天学go-zero学到昏死了,忘记写笔记了。
高质量编程
边界条件是否考虑完备
异常情况处理,稳定性保证
易读易维护
注释应该解释代码的作用
代码实现的原因
代码什么情况下会出错
(未表达出的上下文信息)
性能优化
评估代码性能:
Benchmark
go自身有Benchmark命令来进行性能测试。
基准测试benchmark是go testing库提供的,用来度量程序性能,算法优劣的工具。
指定一个时间(默认是1秒),看测试对象在达到时间上限时,最多能被执行多少次和在此期间测试对象内存分配情况。
如一个斐波那契数列的函数Fib及其基准测试函数如下:
package start
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
package start
import "testing"
// BenchmarkFib10 run 'go test -bench=. -benchmem' to get the benchmark result
func BenchmarkFib10(b *testing.B) {
// run the Fib function b.N times
for n := 0; n < b.N; n++ {
Fib(10)
}
}
进入到文件目录,运行
go test -bench=BenchmarkFib10 -benchmem
表示运行测试BenchmarkFib10,并显示内存
GOMAXPROCS表示逻辑CPU数量。
要求
基准测试的代码文件必须以_test.go结尾
基准测试的函数必须以Benchmark开头
基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数(b *testing.B)
基准测试函数不能有返回值
b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰
最后的for循环很重要,被测试的代码要放到循环里
b.N是基准测试框架提供的,表示循环的次数
常用API
b.StopTimer()
b.StartTimer()
b.ResetTimer()
b.Run(name string, f func(b *B))
b.RunParallel(body func(*PB))
b.ReportAllocs()
b.SetParallelism(p int)
b.SetBytes(n int64)
testing.Benchmark(f func(b *B)) BenchmarkResult
操作命令
go test -bench=BenchmarkFoo
go test -bench=.
// 加上 -bench= 测试名字, .表示运行所有的基准测试,名字可用正则。
go test -bench=BenchmarkFoo -benchtime=5s/10000x
// 加上 -benchtime 设置时间,s表示秒,x表示执行次数
go test -bench=BenchmarkFoo -benchtime=5s -count=3
// 加上 -count 表示几次测试
go test -bench=. -benchmem
// 加上 -benchmem 查看内存
go test -bench=. -benchmem -cpuprofile profile.out
go test -bench=. -benchmem -memprofile memprofile.out
go tool pprof profile.out
go tool pprof memprofile.out
// 结合 pprof 输出查看 cpu和内存。
性能优化建议
Slice
slice 预分配内存。
尽可能在使用make()初始化切片时提供容量信息。(cap)
即 make([]int,0,size) 最好传入size参数
为什么?
提前分配容量,直接指定了容量大小,只有一次分配。因为Silce本质上是底层数组+指针。
切片包括:数组指针,片段长度,片段容量
长度表示左指针至右指针之间的距离,容量表示左指针至底层数组末尾的距离。
切片的扩容机制,append的时候,如果长度增加后超过容量,则将容量增加2倍,同时变换了底层数组。
选取合适的容量,避免了额外的扩容操作。
大内存未释放。
切片操作实际上并不复制切片指向的元素,
所以对一个切片创建一个新切片时,会复用原切片的底层数组。如果原切片比较大,创建的新切片只取了几个元素,这时就会导致原来的底层数组在内存中依然会有引用,得不到释放。
可以使用copy代替re-slice
Map
map预分配内存(make)
字符串处理
使用strings.Builder 拼接字符串
使用+拼接性能最差,strings.Builder,bytes.Buffer 差不多,strings.Buffer更快
为什么要用strings.Builder?
字符串在go中是不可变类型,占用的内存大小固定。
使用+,每次都会重新分配内存。
strings.Builder,bytes.Buffer 底层都是[]byte 数组
内存扩容策略,不需要每次拼接重新分配内存。
而strings.Builder与bytes.Buffer相比,后者转换为字符串时重新申请了一块空间。
前者直接将底层的[]byte转换成了字符串类型返回。
在已知字符串长度时,进一步提升处理性能:
字符串拼接其实和slice差不多,也有相关的支持预分配的方法。
已知字符串长度时,使用builder.Grouw(n * len(str)) 或者buffer中的
buf.Grow(n * len(str))
空结构体
使用空结构体节省内存空间。(空结构体不占用任何内存空间)
可作为各种场景下的占位符使用:
定义map时使用空结构体组为value m := make(map[int]struct{})
空结构体本身具有很强的语义,即这里不需要任何值,仅仅作为占位符。
struct {} {}是一个复合字面量,它构造了一个struct {}类型的值,该值也是空。
可以考虑实现set时(不重复的无序 集合),用map来代替。因为set本身就是一个没有value的map。只需要用到map的键。
即使将map的值设置为bool,也会多占据1个字节空间。
atomic包
在多线程下,使用atomic包的性能要比加锁好
锁的实现是通过操作系统来实现的,属于系统调用
atomic操作是通过硬件实现,效率比锁高
sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量。
对于非数值操作,可以使用atomic.Value,能承载一个interface{}
性能调优原则
依靠数据而不是猜测
定位最大瓶颈而不是细枝末节
不要过早优化,不要过度优化