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次



  • golang性能优化从入门到放弃


经过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)

golang性能优化从入门到放弃

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函数内联,这个在后面还会用到,用下面的命令可以展示编译器分析log
go 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 
(i.e. space that has been allocated but not freed). This is the default.

—inuse_objects

Display the number of in-use objects
(i.e. number of objects that have been allocated but not freed).

—alloc_space

Display the number of allocated megabytes. 
This includes the space that has since been de-allocated. 
Use this if you want to find the main allocation sites in the program.

—alloc_objects

Display the number of allocated objects. 
This includes the objects that have since been de-allocated.
Use this if you want to find the main allocation sites in the program.

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

    展示下面的数据

    golang性能优化从入门到放弃


    第一列flat是它自己直接使用多少内存

    第二列是flat在所有使用内存中的占比,值为flat/total

    第三列是前面的第二列之和,如第n行的三列值为表达topn-1函数直接使用的内存在total中的占比

    第四列cum是它自己直接使用和它调用的函数使用一起内存使用之和

    第五列是cum在所有使用内存中的占比,值为cum/total

    在pprof中执行web命名会绘画出下面的树形图,方框越大使用过的内存越多。

    golang性能优化从入门到放弃

    使用inuse_space展示正在使用的内存
    go tool pprof -inuse_space test.exe mem.pprof
    展示下面的数据

    golang性能优化从入门到放弃

    golang性能优化从入门到放弃

    四.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分析

    golang性能优化从入门到放弃


    第一列flat是函数在执行栈中被命中的次数时间的累加

    第二列是flat/total,标识函数在整个耗时里边的占比

    第三列是前面的第二列之和,如第n行的三列值为表达topn-1函数耗时的占比

    第四列cum是它自己和它调用的函数使用时间之和

    第五列是cum/total,标识它自己和调用的函数在total中的占比

    在pprof中执行web命名会绘画出下面的树形图,方框越大使耗时越大

    golang性能优化从入门到放弃


    网上最火的观看cpu pprof的方式还是通过火焰图来,虽然比较直观,但是按照一套环境比较麻烦,需要按照下面的几个代码
    https://github.com/uber/go-torch
    https://github.com/brendangregg/FlameGraph
    https://www.activestate.com/activeperl
    生成方式如下


    golang性能优化从入门到放弃


    生成结果如下图,因为本例是递归调用,导致命中的基本都是不同层级的fibonacci函数,对展示效果有影响


    golang性能优化从入门到放弃


    附录Uber提供的一张调用及耗时效果都很清晰的火焰图,在真实的生成中遇到的大部分情形都是如此


    golang性能优化从入门到放弃

    五.阻塞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 可以看到阻塞统计

    golang性能优化从入门到放弃

    加上--lines参数的原因是可以看到是那个具体的函数引起的阻塞,从统计中可以看到main.worker中的mutex.Lock()是主要阻塞元凶,也可以用web命令生成图片观看

    golang性能优化从入门到放弃


    其他语言一般采用内部使用数组锁,每个区间共享一把锁,这样减少数据共享一把锁带来的性能影响,但是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条。

    golang性能优化从入门到放弃

    访问http://127.0.0.1/debug/events可以得到eventlog日志,同样也会滚动保存最近的1000条。


    golang性能优化从入门到放弃

    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分析,会自动打开浏览器进行展示

    golang性能优化从入门到放弃


    展示下面的内容


    golang性能优化从入门到放弃


    查看追踪信息,可视化交互展示runtime,可分析每个虚拟处理器执行、goroutine被那种资源阻止等待调度等,这个在下面会详细介绍


    Goroutine 分析,展示创建出的goroutine,里边统计所有的goroutine的执行、等待网络、同步、系统调用、调度等的耗时情况,下面是3个consume goroutine的耗时统计


    golang性能优化从入门到放弃


    3、4、5 网络/同步/系统调用 阻塞分析,展示因为网络同阻塞步、同步阻塞、系统调用阻塞的树形图,和上面描述的pprof一样,下面是同步阻塞的分析树形图。


    golang性能优化从入门到放弃


    6 调度延迟分析器,展示对调度耗时统计数据


    golang性能优化从入门到放弃


    点击 View trace查看追踪信息会展示下面的交互界面



    想要更多的了解golang,后台回复“golang”,

    即可免费获得golang精品课程资料哦!


以上是关于golang性能优化从入门到放弃的主要内容,如果未能解决你的问题,请参考以下文章

Golang 从入门到放弃

kakfa从入门到放弃: golang编程操作kafka

kakfa从入门到放弃: golang编程操作kafka

kakfa从入门到放弃: golang编程操作kafka

Ldap 从入门到放弃

凸优化从入门到放弃(目录)