关于go:一文读懂Go-120引入的PGO性能优化

42次阅读

共计 4354 个字符,预计需要花费 11 分钟才能阅读完成。

背景

Go 1.20 版本于 2023 年 2 月份正式公布,在这个版本里引入了 PGO 性能优化机制。

PGO 的英文全称是 Profile Guided Optimization,基本原理分为以下 2 个步骤:

  • 先对程序做 profiling,收集程序运行时的数据,生成 profiling 文件。
  • 编译程序时启用 PGO 选项,编译器会依据.pgo 文件里的内容对程序做性能优化。

咱们都晓得在编译程序的时候,编译器会对程序做很多优化,包含大家熟知的内联优化(inline optimization)、逃逸剖析(escape analysis)、常数流传(constant propagation)。这些优化是编译器能够间接通过分析程序源代码来实现的。

然而有些优化是无奈通过解析源代码来实现的。

比方一个函数里有很多 if/else 条件分支判断,咱们可能心愿编译器主动帮咱们优化条件分支程序,来放慢条件分支的判断,晋升程序性能。

然而,编译器可能是无奈晓得哪些条件分支进入的次数多,哪些条件分支进入的次数少,因为这个和程序的输出是有关系的。

这个时候,做编译器优化的人就想到了 PGO: Profile Guided Optimization。

PGO 的原理很简略,那就是先把程序跑起来,收集程序运行过程中的数据。而后编译器再依据收集到的程序运行时数据来分析程序的行为,进而做针对性的性能优化。

比方程序能够收集到哪些条件分支进入的次数更多,就把该条件分支的判断放在后面,这样能够缩小条件判断的耗时,晋升程序性能。

那 Go 语言如何应用 PGO 来优化程序的性能呢?咱们接下来看看具体的例子。

示例

咱们实现一个 web 接口/render,该接口以 markdown 文件的二进制格局作为输出,将 markdown 格局转换为 html 格局返回。

咱们借助 gitlab.com/golang-commonmark/markdown 我的项目来实现该接口。

环境搭建

$ go mod init example.com/markdown

新建一个 main.go文件,代码如下:

package main

import (
    "bytes"
    "io"
    "log"
    "net/http"
    _ "net/http/pprof"

    "gitlab.com/golang-commonmark/markdown"
)

func render(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
        return
    }

    src, err := io.ReadAll(r.Body)
    if err != nil {log.Printf("error reading body: %v", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    md := markdown.New(markdown.XHTMLOutput(true),
        markdown.Typographer(true),
        markdown.Linkify(true),
        markdown.Tables(true),
    )

    var buf bytes.Buffer
    if err := md.Render(&buf, src); err != nil {log.Printf("error converting markdown: %v", err)
        http.Error(w, "Malformed markdown", http.StatusBadRequest)
        return
    }

    if _, err := io.Copy(w, &buf); err != nil {log.Printf("error writing response: %v", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
}

func main() {http.HandleFunc("/render", render)
    log.Printf("Serving on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

编译和运行该程序:

$ go mod tidy
$ go build -o markdown.nopgo
$ ./markdown.nopgo
2023/02/25 22:30:51 Serving on port 8080...

程序主目录下新建 input.md 文件,内容能够自定义,合乎 markdown 语法即可。

我演示的例子里用到了 input.md 这个 markdown 文件。

通过 curl 命令发送 markdown 文件的二进制内容给 /render 接口。

$ curl --data-binary @input.md http://localhost:8080/render
<h1>The Go Programming Language</h1>
<p>Go is an open source programming language that makes it easy to build simple,
reliable, and efficient software.</p>
...

能够看到该接口返回了 input.md 文件内容对应的 html 格局。

Profiling

那接下来咱们给 main.go 程序做 profiling,失去程序运行时的数据,而后通过 PGO 来做性能优化。

main.go 里,有 import net/http/pprof 这个库,它会在原来已有的 web 接口 /render 的根底上,新增一个新的 web 接口/debug/pprof/profile,咱们能够通过申请这个 profiling 接口来获取程序运行时的数据。

  • 在程序主目录下,新增 load 子目录,在 load 子目录下新增 main.go 的文件,load/main.go运行时会一直申请下面 ./markdown.nogpo 启动的 server 的 /render 接口,来模拟程序理论运行时的状况。

    $ go run example.com/markdown/load
  • 申请 profiling 接口来获取程序运行时数据。

    $ curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"

​ 期待 30 秒,curl 命令会完结,在程序主目录下会生成 cpu.pprof 文件。

留神:要应用 Go 1.20 版本去编译和运行程序。

PGO 优化程序

$ mv cpu.pprof default.pgo
$ go build -pgo=auto -o markdown.withpgo

go build编译程序的时候,启用 -pgo 选项。

-pgo既能够反对指定的 profiling 文件,也能够反对 auto 模式。

如果是 auto 模式,会主动寻找程序主目录下名为 default.pgo 的 profiling 文件。

Go 官网举荐大家应用 auto 模式,而且把 default.pgo 文件也寄存在程序主目录下保护,这样不便我的项目所有开发者应用 default.pgo 来对程序做性能优化。

Go 1.20 版本里,-pgo选项的默认值是 off,咱们必须增加-pgo=auto 来开启 PGO 优化。

将来的 Go 版本里,官网打算将 -pgo 选项的默认值设置为auto

性能比照

在程序的子目录 load 下新增 bench_test.go 文件,bench_test.go里应用 Go 性能测试的 Benchmark 框架来给 server 做压力测试。

未开启 PGO 优化的场景

启用未开启 PGO 优化的 server 程序:

$ ./markdown.nopgo

开启压力测试:

$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > nopgo.txt

开启 PGO 优化的场景

启用开启了 PGO 优化的 server 程序:

$ ./markdown.withpgo

开启压力测试:

$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > withpgo.txt

综合比照

通过下面压力测试失去的 nopgo.txtwithpgo.txt来做性能比拟。

$ go install golang.org/x/perf/cmd/benchstat@latest
$ benchstat nopgo.txt withpgo.txt
goos: darwin
goarch: amd64
pkg: example.com/markdown/load
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
       │  nopgo.txt  │             withpgo.txt             │
       │   sec/op    │   sec/op     vs base                │
Load-4   447.3µ ± 7%   401.3µ ± 1%  -10.29% (p=0.000 n=20)

能够看到,应用 PGO 优化后,程序的性能晋升了 10.29%,这个晋升成果十分可观。

在 Go 1.20 版本里,应用 PGO 之后,通常程序的性能能够晋升 2%-4% 左右。

后续的版本里,编译器还会持续优化 PGO 机制,进一步晋升程序的性能。

总结

Go 1.20 版本引入了 PGO 来让编译器对程序做性能优化。PGO 应用分 2 个步骤:

  • 先失去一个 profiling 文件。
  • 应用 go build 编译时开启 PGO 选项,通过 profiling 文件来领导编译器对程序做性能优化。

在生产环境里,咱们能够收集近段时间的 profiling 数据,而后通过 PGO 去优化程序,以晋升零碎解决性能。

更多对于 PGO 的应用阐明和最佳实际能够参考 profile-guided optimization user guide。

源代码地址:pgo optimization source code。

举荐浏览

  • Go 1.20 来了,看看都有哪些变动
  • Go 面试题系列,看看你会几题
  • Go 常见谬误和最佳实际系列

开源地址

文章和示例代码开源在 GitHub: Go 语言高级、中级和高级教程。

公众号:coding 进阶。关注公众号能够获取最新 Go 面试题和技术栈。

集体网站:Jincheng’s Blog。

知乎:无忌。

福利

我为大家整顿了一份后端开发学习材料礼包,蕴含编程语言入门到进阶常识(Go、C++、Python)、后端开发技术栈、面试题等。

关注公众号「coding 进阶」,发送音讯 backend 支付材料礼包,这份材料会不定期更新,退出我感觉有价值的材料。还能够发送音讯「进群」,和同行一起交流学习,答疑解惑。

References

  • https://go.dev/blog/pgo-preview

正文完
 0