Go 博客

基于配置文件的优化预览

Michael Pratt
2023 年 2 月 8 日

当您构建 Go 二进制文件时,Go 编译器会执行优化以尝试生成它所能生成的性能最佳的二进制文件。例如,常量传播可以在编译时计算常量表达式,避免运行时计算成本。逃逸分析避免为局部范围对象分配堆内存,避免 GC 负担。内联将简单函数的主体复制到调用者中,通常可以使调用者进一步优化(例如,额外的常量传播或更好的逃逸分析)。

Go 在每次发布中都会改进优化,但这并不总是容易。一些优化是可调的,但编译器不能仅仅在每个函数上“调到 11”,因为过于激进的优化实际上会损害性能或导致构建时间过长。其他优化要求编译器对函数中的“常见”和“不常见”路径做出判断。编译器必须根据静态启发式方法做出最佳猜测,因为它无法知道哪些情况在运行时会很常见。

或者可以吗?

在没有关于代码如何在生产环境中使用的明确信息的情况下,编译器只能对包的源代码进行操作。但我们确实有一个工具来评估生产行为:分析。如果我们向编译器提供一个分析文件,它可以做出更明智的决定:更积极地优化最常用的函数,或更准确地选择常见情况。

将应用程序行为的分析文件用于编译器优化称为基于配置文件的优化 (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 https://127.0.0.1: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://127.0.0.1:8080/debug/pprof/profile?seconds=30"

完成后,关闭负载生成器和服务器。

使用分析文件

我们可以要求 Go 工具链使用-pgo标志对go build使用 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 已发布!
博客索引