温习一下 PGO
Profile-guided optimization (PGO),PGO 是计算机编程中的一种编译器优化技术,借助配置文件来引导编译,达到提高程序运行时性能的目的。
翻译过来是使用配置文件引导的优化,能提供应用程序的性能。也被称为:
- profile-directed feedback(PDF)
- feedback-directed optimization(FDO)
该项优化是一个通用技术,不局限于某一门语言。像是我们常见很多应用都有所使用其来优化。如下几个案例:
- Chrome 浏览器,在 64 位版本的 Chrome 中从 53 版开始启用 PGO, 32 位版在 54 版中启用。
- AutoFDO 进行了 PGO 的优化,直接将某数据中心中的 C/C++ 程序的性能提高了 5-15%(不用改业务代码)。
Go 怎么读取 PGO
PGO 第一个版本先支持的 pprof CPU,直接读取 pprof CPU profile 文件来完成优化。
有以下两种方式:
- 手动指定:Go 工具链在 go build 子命令增加了
-pgo=<path>
参数,用于显式指定用于 PGO 构建的 profile 文件位置。 - 自动指定:当 Go 工具链在主模块目录下找到 default.pgo 的配置文件时,将会自动启用 PGO。
快速 Demo
初始化应用程序
首先我们创建一个 Demo 目录,用于做一系列的实验。执行如下命令:
$ mkdir pgo-demo && cd pgo-demo
初始化模块路径和拉取程序所需的依赖:
$ go mod init example.com/markdown go: creating new go.mod: module example.com/markdown $ go get gitlab.com/golang-commonmark/markdown@bf3e522c626a
创建 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 build -o markdown.nopgo $ ./markdown.nopgo 2023/10/02 13:55:40 Serving on port 8080...
运行起来后进行验证,这是一个将 Markdown 转换为 HTML 的应用程序。
我们从 GitHub 上拉取一份 markdown 文件并给到该程序进行转换。如下命令:
$ curl -o README.md -L "https://raw.githubusercontent.com/golang/go/c16c2c49e2fa98ae551fc6335215fadd62d33542/README.md" $ curl --data-binary @README.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> ...
如果正常则说明运行没问题。
收集 PGO 所需的配置文件
一般情况下,我们可以通过生产、测试环境的 pprof 采集所需的 profile 文件,用于做 PGO 的配置文件。
但由于示例没有对应的生产环境。本次快速 Demo,Go 官方提供了一个简单的程序来快速的发压。
在确保前面小节的 pgo-demo 程序正常运行的情况下。运行如下命令,启动一个发压程序:
$ go run github.com/prattmic/markdown-pgo/load@latest
收集对应的 profile 文件:
$ curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"
生成了一个 cpu.pprof 文件,可以在后续使用。
应用程序使用 PGO
前面我们有提到,当模块目录下包含 default.pgo 时。Go 工具链就会自动应用 PGO。
我们只需要将前面的 cpu.pprof 修改一下即可。执行如下命令:
$ mv cpu.pprof default.pgo $ go build -o markdown.withpgo
编译成功后,使用如下命令验证是否正常:
$ go version -m markdown.withpgo markdown.withpgo: go1.21.1 path example.com/markdown mod example.com/markdown (devel) ... build GOOS=darwin build GOAMD64=v1 build -pgo=/Users/eddycjy/app/go/pgo-demo/default.pgo
可以看到最后的build -pgo=...
,代表该应用程序成功应用了我们的 default.pgo 文件(启用 PGO)。
总结
PGO 作为 Go 新版本的一个性能好帮手,在官方给出的数据中启用 PGO 后,性能能够得到一定的提升。但也会带来其他方面(CPU、大小等)的开销增加。
如本文的例子中,官方给出的数据是程序性能提升了约 2~4%,CPU 使用率会带来 2~7% 的开销增加。也可能会导致构建时长变长一些、编译后的二进制文件会稍微大一些。
面对一些场景,PGO 是一个不错的性能优化方式。但有利必有弊,就看这个应用程序的类型和综合取舍了。