golang性能优化从入门到放弃
Posted 腾讯课堂Coding学院
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang性能优化从入门到放弃相关的知识,希望对你有一定的参考价值。
基础
一.基调测试
1.介绍
通过基准测试才能知道性能(CPU和内存效率),这是golang代码优化的第一步。下面以简单的整数转字符串来说明怎么使用。
package mainimport ( "fmt" "strconv" "testing")func BenchmarkSprintf(b *testing.B) { for i := 0; i < b.N; i++ { fmt.Sprintf("%d", 10) } }func BenchmarkStrconv(b *testing.B) { for i := 0; i < b.N; i++ { strconv.FormatInt(10, 10) } }func BenchmarkItoa(b *testing.B) { for i := 0; i < b.N; i++ { strconv.Itoa(10) } }
执行方法go test -bench=. -benchtime=5s -benchmem
-bench 参数表示适配那些测试函数,如-bench=S.则会跑BenchmarkSprintf和BenchmarkStrconv两个测试,-bench=BenchmarkStrconv则会只跑BenchmarkStrconv测试。
-benchtime 默认是1s,跑每个基准的大概限时,基准会先跑100、1000等数量的测试预估时间,然后调整b.N使其差不多在-benchtime时间左右完成。
-benchmem 同时测试内容的使用情况。
执行的结果如下
第一列是函数名字+运行时对应的GOMAXPROCS的值,可以用-cpu=2来设置成其他的值
第二列是一种执行多少次
第三列是每次执行的耗时
第四列是每次执行需要分配的内存数量(堆上)
第五列是每次执行需要分配的对象个数(堆上)
从结果中可以得到结论FormatInt速度更快一些,而Sprintf耗时是FormatInt的20倍,原因是Sprintf每次执行需要分配一个对象,具体可以去看fmt.Sprintf的实现源码,Itoa 其实是FormatInt的简单封装,因为多了一次函数调用耗时,性能也有部分损耗。
2.benchstat
计算机系统是个动态的类生命体,每次的基准测试结果都有变数,此时就需要我们多测试几次得到平均值,这里推荐下面的工具golang.org/x/perf/cmd/benchstat, 可以到https://golangtc.com/download/package 去下载,编译后会生成一个benchstat的可执行文件。
-count 默认是1,参数来设置跑基准的次数,本例设置每个基准跑10次
经过benchstat处理后,我们可以得到Sprintf每次的耗时在 76.9ns(加减2%)。
3.指导生产
下面的实际生产用基准测试来指导json的选择,本例只对比原生json库和easyjson库(非simplejson),其他的读者可以自行测试
为了延时benchstat的对比功能,本测试分两次进行,一次测试原生一次测试easyjson库。
测试使用的例子
type Student struct { Id int64 `json:"id"` Addr string `json:"addr"` Name string `json:"name"`}var obejctStudent = Student{10086, "guangdong", "hanmeimei"}var jsonStudent = []byte(`{"id":10000,"addr":"beijin","name":"lilei"}`)
自带的json库测试,执行函数为json.Marshal(obejctStudent)
和 json.Unmarshal(jsonStudent, &obejctStudent)
。
easyjson库测试(已经生成student_easyjson.go 序列化函数文件)obejctStudent.MarshalJSON()
和 obejctStudent.UnmarshalJSON(jsonStudent)
。
Encode时间减少80%,Decode减少74%,使用的内存也改善很大,究其原因还是因为easyjson是预先处理结构体,通过字符串连接来实现,而原生的是通过费资源的反射来实现
二.变量逃逸分析
1.介绍
通过基准测试发现在在堆上分配内存的操作都比较耗时,那怎么确定变量是在堆上分配的呢?先看下面的代码:
package main//go:noinlinefunc test() *int { inner := 10 return &inner }func main() { ret := test() *ret = 1}
上面这段代码,如果是C/C++的话,inner是在栈上分配的内存,在test函数返回后会回收,会给警告执行出错。golang可好好的执行,因为go的编译器会做逃逸分析,如果发现变量的作用域跑远了,会在堆上分配变量空间。
代码里边的//go:noinline
为了防止编辑器将test函数内联,这个在后面还会用到,用下面的命令可以展示编译器分析loggo tool compile -m main.go
逃逸分析对内存管理进行优化和简化,好处就是减少程序员的脑力,但在实际生产中会导致一些问题,把原本可以在栈上分配的空间提到堆上,增大GC的压力。
2.缺陷
下面是在实际生产中遇到的两种常见缺陷,如果GC压力比较大的情况下可以改变实现方式。
a.传递回调函数导致变量逃逸
package main//go:noinlinefunc swap(*int, *int) {}func test() { x, y := 1, 2 swap(&x, &y) }func callbackTest(f func(*int, *int)) { s, b := 1, 2 f(&s, &b) }func main() { test() callbackTest(swap) }
这种传递回调函数的形式在开发中经常用到,会带来非必要的逃逸。
b.针对接口的方法调用导致变量逃逸
package maintype Bird interface { sing() }type Duck struct {}//go:noinlinefunc (me Duck) sing() {}func test() { duck := Duck{} var bird Bird = duck duck.sing() }func main() { test() }
如果想程序扩展性好,接口是必不可少的,但是这回带来少量的GC压力。
中级
三.内存pprof
需要加上可执行的程序和生成的pprof,才能展示内存使用概览,支持下面4个参数
参数
说明
—inuse_space |
Display the number of in-use megabytes |
—inuse_objects |
Display the number of in-use objects |
—alloc_space |
Display the number of allocated megabytes. |
—alloc_objects |
Display the number of allocated objects. |
space和objects的区别,space是展示内存大小,单位是Byte;而objects展示的对象,单位是个,下面以inuse_space和alloc_space为来说明inuse和alloc参数的区别。
inuse_space展示的是现在正在使用的内存,被分配但是还没有被释放,这个是默认参数。
alloc_space展示的是程序启动到现在的分配内存,包括那些已经被释放的内存,如果系统GC负担大,我们一般用这个来定位代码中创建临时变量的大户。
测试代码
package mainimport ( "log" "os" "runtime" "runtime/pprof")var usingBuffer []bytefunc main() { f, err := os.Create("mem.prof") if err != nil { log.Fatal("could not create memory profile: ", err) } defer f.Close() runtime.MemProfileRate = 1 allocateUsingMem() allocateFlashMem() runtime.GC() if err := pprof.Lookup("heap").WriteTo(f, 0); err != nil { log.Fatal("could not write memory profile: ", err) } }//go:noinlinefunc allocateUsingMem() { usingBuffer = make([]byte, 100*10000) }func allocateFlashMem() []byte { var buf []byte for i := 0; i < 100; i++ { buf = make([]byte, 10000) } return buf }
go tool pprof -alloc_space test.exe mem.pprof
展示下面的数据
第一列flat是它自己直接使用多少内存
第二列是flat在所有使用内存中的占比,值为flat/total
第三列是前面的第二列之和,如第n行的三列值为表达topn-1函数直接使用的内存在total中的占比
第四列cum是它自己直接使用和它调用的函数使用一起内存使用之和
第五列是cum在所有使用内存中的占比,值为cum/total
在pprof中执行web命名会绘画出下面的树形图,方框越大使用过的内存越多。
使用inuse_space展示正在使用的内存go tool pprof -inuse_space test.exe mem.pprof
展示下面的数据
四.CPU pprof
CPU pprof是runtime定时记录现在执行的goroutine执行栈,累加各个函数出现的次数得到的结果。越是占用时间长越热的函数命中的概率越大, CPU pprof用来定位程序中的瓶颈函数,定位出来后可以按照前面讲的进行部分优化。
例子是递归的斐波纳契数列来测试,在中间加入了sum函数,在实际生成中一般不如此编码。
package mainimport ( "os" "runtime/pprof")func main() { f, err := os.Create("cpu.pprof") if err != nil { panic(err) } pprof.StartCPUProfile(f) fibonacci(40) pprof.StopCPUProfile() }//go:noinlinefunc sum(a, b int) int { return a + b }func fibonacci(num int) int { switch num { case 0: return 0 case 1: return 1 default: return sum(fibonacci(num-1), fibonacci(num-2)) } }
执行完成后通过下面的命令进入CPU pprof分析
第一列flat是函数在执行栈中被命中的次数时间的累加
第二列是flat/total,标识函数在整个耗时里边的占比
第三列是前面的第二列之和,如第n行的三列值为表达topn-1函数耗时的占比
第四列cum是它自己和它调用的函数使用时间之和
第五列是cum/total,标识它自己和调用的函数在total中的占比
在pprof中执行web命名会绘画出下面的树形图,方框越大使耗时越大
网上最火的观看cpu pprof的方式还是通过火焰图来,虽然比较直观,但是按照一套环境比较麻烦,需要按照下面的几个代码
https://github.com/uber/go-torch
https://github.com/brendangregg/FlameGraph
https://www.activestate.com/activeperl
生成方式如下
生成结果如下图,因为本例是递归调用,导致命中的基本都是不同层级的fibonacci函数,对展示效果有影响
附录Uber提供的一张调用及耗时效果都很清晰的火焰图,在真实的生成中遇到的大部分情形都是如此
五.阻塞pprof
pprof block保存用户程序中的Goroutine阻塞事件的记录,需要通过调用runtime.SetBlockProfileRate
来激活
golang的goroutine发生阻塞回切到另一个goroutine执行,所以golang的阻塞对比其他的开发语言影响较小,但是过大的阻塞时间消耗会影响接口的反馈速度,可以借助pprof block定位出热门阻塞资源进行代码结构优化。
例子代码如下
package mainimport ( "os" "runtime" "runtime/pprof" "sync")type LockerMap struct { sync.Mutex datas map[int]int}var mutex sync.Mutexvar wg sync.WaitGroupvar lmap = LockerMap{datas: make(map[int]int, 10000)}func checkErr(err error) { if err != nil { panic(err) } }func worker() { defer wg.Done() for i := 0; i < 10000; i++ { mutex.Lock() lmap.datas[i] = i mutex.Unlock() } }func main() { runtime.SetBlockProfileRate(1) for i := 0; i < 100; i++ { wg.Add(1) go worker() } wg.Wait() p := pprof.Lookup("block") f, err := os.Create("block.pprof") checkErr(err) defer f.Close() err = p.WriteTo(f, 0) checkErr(err) }
这段代码的主要描述是定义了一个简单带锁的map,每次操作都加锁进行,需要用go tool pprof block.pprof
可以看到阻塞统计
加上--lines
参数的原因是可以看到是那个具体的函数引起的阻塞,从统计中可以看到main.worker中的mutex.Lock()是主要阻塞元凶,也可以用web命令生成图片观看
其他语言一般采用内部使用数组锁,每个区间共享一把锁,这样减少数据共享一把锁带来的性能影响,但是golang的作者说过会考虑,除了1.9后出现的sync.Map没有进一步消息。
sync.Map采用了另一种常用的解决方式双副本的形式来解决并发读写,我们上面这种只写的测试在sync.Map表现会更差,双副本的方式主要解决多读少写的场景,期待数组锁的的map的出现。
先阶段如果是多谢的Map,可以创建多个map,对key取模后存储在不同的map来提高效率。
高级
六.net.trace
首先net trace区别与runtime trace,net trace用户服务端请求追踪,可以用来展示单次请求后服务端统计和长期执行的程序中的event统计,这些统计都是开发者埋点自己打印进去的。
而runtime trace记录的所有的运行时事件,用户诊断性能问题时(如延迟,并行化和竞争异常等),这个以后的篇幅会讲到。
package main import ( "net/http" "time" "golang.org/x/net/trace")var elog trace.EventLogfunc helloHandler(w http.ResponseWriter, r *http.Request) { elog.Printf("helloHandler visit by %s", r.RemoteAddr) ret := "word" r.ParseForm() tr := trace.New("request", r.URL.Path) defer tr.Finish() tr.LazyPrintf("user request:%s", r.Form.Encode()) tr.LazyPrintf("return result:%s", ret) w.Write([]byte(ret)) } func weiHandler(w http.ResponseWriter, r *http.Request) { elog.Printf("weiHandler visit by %s", r.RemoteAddr) ret := "你好!" r.ParseForm() tr := trace.New("request", r.URL.Path) defer tr.Finish() tr.LazyPrintf("user request:%s", r.Form.Encode()) tr.LazyPrintf("return result:%s", ret) w.Write([]byte(ret)) } func main() { elog = trace.NewEventLog("server.event", "elog") defer elog.Finish() elog.Printf("server start:%s", time.Now().Format("20060102 15:03:04")) http.HandleFunc("/hello", helloHandler) http.HandleFunc("/wei", weiHandler) elog.Printf("registe handler ok ") http.ListenAndServe(":80", nil) }
然后请求下面两个网址各一次:http://127.0.0.1/wei?a=applen&b=banana
http://127.0.0.1/hello?a=b&b=c
访问http://127.0.0.1/debug/requests
可以得到trace日志,并不是报错所有的日志,trace会滚动保证每个title只有最近的10条。
访问http://127.0.0.1/debug/events
可以得到eventlog日志,同样也会滚动保存最近的1000条。
net trace的能提供给我们一些简单的统计和event记录,在重要的逻辑段预埋eventlog,有利于我们了解我们的服务即时发现问题。
七.runtime.trace
就如上面提交runtime.trace,记录的所有的运行时事件,这个工具对goroutine调度、系统调用等进行trace,涉及此些方面的问题,一般解决都比较麻烦。
package mainimport ( "fmt" "os" "runtime/trace" "sync")const testNumber = 10var queue = make(chan int, 2)var wg sync.WaitGroupfunc consume() { for data := range queue { fmt.Println("consume", data) } wg.Done() }func product() { for i := 0; i < testNumber; i++ { queue <- i } close(queue) wg.Done() }func main() { f, err := os.Create("trace.out") if err != nil { panic(err) } defer f.Close() err = trace.Start(f) if err != nil { panic(err) } defer trace.Stop() wg.Add(4) for i := 0; i < 3; i++ { go consume() } go product() wg.Wait() }
用go tool trace trace.out即可进行trace分析,会自动打开浏览器进行展示
展示下面的内容
查看追踪信息,可视化交互展示runtime,可分析每个虚拟处理器执行、goroutine被那种资源阻止等待调度等,这个在下面会详细介绍
Goroutine 分析,展示创建出的goroutine,里边统计所有的goroutine的执行、等待网络、同步、系统调用、调度等的耗时情况,下面是3个consume goroutine的耗时统计
3、4、5 网络/同步/系统调用 阻塞分析,展示因为网络同阻塞步、同步阻塞、系统调用阻塞的树形图,和上面描述的pprof一样,下面是同步阻塞的分析树形图。
6 调度延迟分析器,展示对调度耗时统计数据
点击 View trace查看追踪信息会展示下面的交互界面
想要更多的了解golang,后台回复“golang”,
即可免费获得golang精品课程资料哦!
以上是关于golang性能优化从入门到放弃的主要内容,如果未能解决你的问题,请参考以下文章