Go 博客
基于配置文件的优化预览
当您构建 Go 二进制文件时,Go 编译器会执行优化,以尝试生成性能最佳的二进制文件。例如,常量传播可以在编译时评估常量表达式,从而避免运行时评估成本。逃逸分析可以避免为局部作用域对象分配堆内存,从而避免 GC 开销。内联是将简单函数的函数体复制到调用者中,这通常可以使调用者进行进一步的优化(例如,额外的常量传播或更好的逃逸分析)。
Go 的优化在版本之间不断改进,但这并非易事。一些优化是可以调整的,但编译器不能对每个函数都“调到 11”,因为过于激进的优化实际上可能会损害性能或导致过长的构建时间。其他优化需要编译器对函数中“常见”和“不常见”的路径做出判断。编译器必须根据静态启发式方法进行最佳猜测,因为它无法知道在运行时哪些情况是常见的。
还是说,它能知道?
在没有关于代码在生产环境中如何使用的确切信息的情况下,编译器只能操作包的源代码。但是,我们有一个工具可以评估生产环境的行为:分析。如果我们向编译器提供分析信息,它就可以做出更明智的决定:更积极地优化最常使用的函数,或者更准确地选择常见情况。
使用应用程序行为分析信息进行编译器优化被称为基于配置文件的优化 (Profile-Guided Optimization, PGO)(也称为面向反馈的优化 (Feedback-Directed Optimization, 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 https://: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 "https://: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 通常可以将 CPU 使用率提高 2% 到 4%。分析文件包含关于应用程序行为的大量信息,而 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 发布!
博客索引