Go 博客
配置文件引导优化预览
当你构建一个 Go 二进制文件时,Go 编译器会执行优化,试图生成性能最佳的二进制文件。例如,常量传播可以在编译时评估常量表达式,避免运行时评估开销。逃逸分析可以避免局部作用域对象的堆分配,从而减少 GC 开销。函数内联会将简单函数的主体复制到调用方,这通常能进一步优化调用方(例如额外的常量传播或更好的逃逸分析)。
Go 在版本迭代中不断改进优化,但这并非易事。一些优化是可调整的,但编译器不能简单地对每个函数都“开足马力”,因为过度激进的优化实际上会损害性能或导致过长的构建时间。其他优化则要求编译器判断函数中的“常见”和“非常见”路径。由于无法知道哪些情况在运行时会更常见,编译器必须基于静态启发式方法做出最佳猜测。
或者它可以吗?
如果没有关于代码在生产环境中如何使用的明确信息,编译器只能对包的源代码进行操作。但是我们有一个工具可以评估生产行为:性能剖析。如果我们向编译器提供一份性能剖析数据,它可以做出更明智的决策:更积极地优化最常用的函数,或更准确地选择常见情况。
将应用程序行为的性能剖析数据用于编译器优化称为 配置文件引导优化 (PGO)(也称为反馈导向优化 (FDO))。
Go 1.20 包含了对 PGO 的初步支持作为预览版。有关完整的文档,请参阅配置文件引导优化用户指南。目前仍存在一些可能阻碍生产环境使用的问题,但我们非常希望您能试用并向我们发送您遇到的任何反馈或问题。
示例
让我们构建一个将 Markdown 转换为 HTML 的服务:用户将 Markdown 源上传到 /render
,它返回转换后的 HTML。我们可以使用 gitlab.com/golang-commonmark/markdown
轻松实现这一点。
设置
$ go mod init 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.exe
$ ./markdown.nopgo.exe
2023/01/19 14:26:24 Serving on port 8080...
让我们尝试从另一个终端发送一些 Markdown。我们可以使用 Go 项目的 README 作为示例文档。
$ 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 重建,看看是否能获得更好的性能。
在 main.go
中,我们导入了 net/http/pprof,它自动为服务器添加了一个 /debug/pprof/profile
端点,用于获取 CPU 性能剖析数据。
通常,您会希望从生产环境中收集性能剖析数据,以便编译器能够获得生产环境行为的代表性视图。由于本例没有“生产”环境,我们将创建一个简单的程序来生成负载,同时收集性能剖析数据。将此程序的源代码复制到 load/main.go
并启动负载生成器(确保服务器仍在运行!)。
$ go run example.com/markdown/load
在它运行时,从服务器下载性能剖析数据
$ curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"
一旦完成,杀死负载生成器和服务器。
使用性能剖析数据
我们可以使用 go build
的 -pgo
标志要求 Go 工具链使用 PGO 进行构建。-pgo
接受要使用的性能剖析文件的路径,或 auto
,这将使用主包目录中的 default.pgo
文件。
我们建议将 default.pgo
配置文件提交到您的仓库。将配置文件与源代码一起存储可以确保用户只需通过拉取仓库(无论是通过版本控制系统还是通过 go get
)即可自动访问配置文件,并且构建仍然可重现。在 Go 1.20 中,默认设置为 -pgo=off
,因此用户仍需要添加 -pgo=auto
,但未来的 Go 版本预计会将默认值更改为 -pgo=auto
,自动让构建二进制文件的任何人都能受益于 PGO。
让我们构建
$ mv cpu.pprof default.pgo
$ go build -pgo=auto -o markdown.withpgo.exe
评估
我们将使用负载生成器的 Go 基准测试版本来评估 PGO 对性能的影响。将此基准测试复制到 load/bench_test.go
。
首先,我们将在不使用 PGO 的情况下对服务器进行基准测试。启动该服务器
$ ./markdown.nopgo.exe
在它运行时,运行多次基准测试迭代
$ go test example.com/markdown/load -bench=. -count=20 -source ../README.md > nopgo.txt
一旦完成,杀死原始服务器并启动使用 PGO 的版本
$ ./markdown.withpgo.exe
在它运行时,运行多次基准测试迭代
$ go test example.com/markdown/load -bench=. -count=20 -source ../README.md > withpgo.txt
一旦完成,让我们比较结果
$ go install golang.org/x/perf/cmd/benchstat@latest
$ benchstat nopgo.txt withpgo.txt
goos: linux
goarch: amd64
pkg: example.com/markdown/load
cpu: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
│ nopgo.txt │ withpgo.txt │
│ sec/op │ sec/op vs base │
Load-12 393.8µ ± 1% 383.6µ ± 1% -2.59% (p=0.000 n=20)
新版本快了大约 2.6%!在 Go 1.20 中,工作负载启用 PGO 后通常能获得 2% 到 4% 的 CPU 使用率改进。配置文件包含有关应用程序行为的大量信息,而 Go 1.20 仅仅通过使用这些信息进行内联来初探其潜力。未来的版本将继续改进性能,因为编译器的更多部分将利用 PGO。
后续步骤
在这个例子中,收集性能剖析数据后,我们使用与原始构建中完全相同的源代码重建了服务器。在实际场景中,总有持续的开发。因此,我们可能会从正在运行上周代码的生产环境中收集性能剖析数据,并用它来构建今天的源代码。这是完全没问题的!Go 中的 PGO 可以轻松处理源代码的微小更改。
有关使用 PGO 的更多信息、最佳实践以及需要注意的事项,请参阅配置文件引导优化用户指南。
请向我们发送您的反馈!PGO 仍处于预览阶段,我们很乐意听取任何关于使用困难、工作不正确等方面的问题。请在 go.dev/issue/new 提交问题。
致谢
将配置文件引导优化添加到 Go 是一项团队努力,我特别要感谢 Uber 的 Raj Barik 和 Jin Lin,以及 Google 的 Cherry Mui 和 Austin Clements 做出的贡献。这种跨社区协作是使 Go 变得伟大的关键部分。
下一篇文章:所有可比较类型
上一篇文章:Go 1.20 发布了!
博客索引