配置文件引导优化
从 Go 1.20 开始,Go 编译器支持配置文件引导优化 (PGO) 以进一步优化构建。
目录
概览
收集配置文件
使用 PGO 构建
注意事项
常见问题
附录:其他配置文件来源
概览
配置文件引导优化 (PGO),也称为反馈定向优化 (FDO),是一种编译器优化技术,它将应用程序代表性运行的信息(配置文件)反馈给编译器,用于应用程序的下一次构建。编译器利用这些信息做出更明智的优化决策。例如,编译器可能会决定更积极地内联配置文件显示频繁调用的函数。
在 Go 中,编译器使用 CPU pprof 配置文件作为输入配置文件,例如来自 runtime/pprof 或 net/http/pprof。
截至 Go 1.22,对一组代表性 Go 程序的基准测试表明,使用 PGO 构建可将性能提高约 2-14%。我们预计随着未来 Go 版本中更多优化利用 PGO,性能提升将普遍随时间增加。
收集配置文件
Go 编译器期望 CPU pprof 配置文件作为 PGO 的输入。Go 运行时生成的配置文件(例如来自 runtime/pprof 和 net/http/pprof)可以直接用作编译器输入。也可以使用/转换来自其他分析系统的配置文件。有关更多信息,请参阅附录。
为了获得最佳结果,重要的是配置文件要代表应用程序生产环境中的实际行为。使用不具代表性的配置文件可能会导致生成的二进制文件在生产环境中几乎没有改进。因此,建议直接从生产环境收集配置文件,这是 Go PGO 的主要设计方法。
典型的工作流程如下:
- 构建并发布初始二进制文件(不带 PGO)。
- 从生产环境收集配置文件。
- 当需要发布更新的二进制文件时,从最新源代码构建并提供生产配置文件。
- 转到 2
Go PGO 通常能够稳健地处理已分析的应用程序版本与使用配置文件构建的版本之间的偏差,以及使用从已优化二进制文件收集的配置文件进行构建。这使得这种迭代生命周期成为可能。有关此工作流程的更多详细信息,请参阅 AutoFDO 部分。
如果难以或无法从生产环境收集(例如,分发给最终用户的命令行工具),也可以从代表性基准中收集。请注意,构建代表性基准通常非常困难(随着应用程序的演变,保持其代表性也同样困难)。特别是,微基准通常不适合 PGO 分析,因为它们只运行应用程序的一小部分,这在应用于整个程序时只会带来很小的收益。
使用 PGO 构建
标准的构建方法是将 pprof CPU 配置文件以文件名 default.pgo
存储在已分析二进制文件的主包目录中。默认情况下,go build
将自动检测 default.pgo
文件并启用 PGO。
建议将配置文件直接提交到源代码仓库,因为配置文件是构建的重要输入,对于可重现(且高性能!)的构建至关重要。与源代码一起存储可简化构建体验,因为除了获取源代码之外,无需额外步骤来获取配置文件。
对于更复杂的场景,go build -pgo
标志控制 PGO 配置文件选择。此标志默认为 -pgo=auto
,用于上述 default.pgo
行为。将标志设置为 -pgo=off
则完全禁用 PGO 优化。
如果您不能使用 default.pgo
(例如,一个二进制文件的不同场景需要不同的配置文件,无法将配置文件与源代码一起存储等),您可以直接传递要使用的配置文件路径(例如,go build -pgo=/tmp/foo.pprof
)。
注意:传递给 -pgo
的路径适用于所有主包。例如,go build -pgo=/tmp/foo.pprof ./cmd/foo ./cmd/bar
将 foo.pprof
应用于二进制文件 foo
和 bar
,这通常不是您想要的。通常,不同的二进制文件应有不同的配置文件,通过单独的 go build
调用传入。
注意:在 Go 1.21 之前,默认值为 -pgo=off
。PGO 必须显式启用。
注意事项
从生产环境收集代表性配置文件
如收集配置文件中所述,您的生产环境是应用程序代表性配置文件的最佳来源。
最简单的入门方法是将 net/http/pprof 添加到您的应用程序中,然后从服务的任意实例获取 /debug/pprof/profile?seconds=30
。这是一个很好的开始方式,但存在一些可能不具代表性的情况:
-
即使通常很忙,此实例在被分析时可能未执行任何操作。
-
流量模式可能全天变化,导致行为全天变化。
-
实例可能执行长时间运行的操作(例如,5 分钟执行操作 A,然后 5 分钟执行操作 B 等)。30 秒的配置文件可能只覆盖单一操作类型。
-
实例可能无法获得公平的请求分布(某些实例接收的某种类型请求多于其他实例)。
更稳健的策略是在不同时间从不同实例收集多个配置文件,以限制单个实例配置文件之间差异的影响。然后可以将多个配置文件合并为单个配置文件以用于 PGO。
许多组织运行“持续分析”服务,自动执行这种全舰队采样分析,然后可将其用作 PGO 配置文件的来源。
合并配置文件
pprof 工具可以像这样合并多个配置文件:
$ go tool pprof -proto a.pprof b.pprof > merged.pprof
这种合并实际上是输入样本的简单求和,无论配置文件的墙钟持续时间如何。因此,在分析应用程序的一小段时间(例如,无限期运行的服务器)时,您可能希望确保所有配置文件具有相同的墙钟持续时间(即,所有配置文件都收集 30 秒)。否则,具有较长墙钟持续时间的配置文件将在合并配置文件中过度表示。
AutoFDO
Go PGO 旨在支持“AutoFDO”风格的工作流程。
让我们仔细看看收集配置文件中描述的工作流程:
- 构建并发布初始二进制文件(不带 PGO)。
- 从生产环境收集配置文件。
- 当需要发布更新的二进制文件时,从最新源代码构建并提供生产配置文件。
- 转到 2
这听起来似乎很简单,但有几个重要的特性需要注意:
-
开发总是在进行中,因此已分析二进制文件(步骤 2)的源代码可能与正在构建的最新源代码(步骤 3)略有不同。Go PGO 旨在对此具有稳健性,我们称之为源代码稳定性。
-
这是一个闭环。也就是说,在第一次迭代之后,已分析的二进制文件已经通过前一次迭代的配置文件进行了 PGO 优化。Go PGO 也旨在对此具有稳健性,我们称之为迭代稳定性。
源代码稳定性是通过启发式方法实现的,用于将配置文件中的样本与编译源代码进行匹配。因此,对源代码的许多更改,例如添加新函数,对匹配现有代码没有影响。当编译器无法匹配更改的代码时,一些优化会丢失,但请注意,这是一种优雅降级。单个函数匹配失败可能会失去优化机会,但整体 PGO 收益通常分布在许多函数中。有关匹配和降级的更多详细信息,请参阅源代码稳定性部分。
迭代稳定性是防止连续 PGO 构建中出现可变性能循环(例如,构建 #1 很快,构建 #2 慢,构建 #3 很快等)。我们使用 CPU 配置文件来识别热函数以进行优化。理论上,一个热函数可能会被 PGO 加速到如此程度,以至于它在下一个配置文件中不再显示为热点,因此不会被优化,从而使其再次变慢。Go 编译器对 PGO 优化采取保守方法,我们相信这可以防止显著的差异。如果您确实观察到这种不稳定性,请在 go.dev/issue/new 提交问题。
源代码稳定性和迭代稳定性共同消除了两阶段构建的需求,即首先对未经优化的构建进行分析作为金丝雀,然后使用 PGO 进行生产重建(除非绝对需要峰值性能)。
源代码稳定性和重构
如上所述,Go 的 PGO 会尽力尝试继续将旧配置文件中的样本与当前源代码进行匹配。具体来说,Go 使用函数内的行偏移量(例如,在函数 foo 的第 5 行调用)。
许多常见更改不会破坏匹配,包括:
-
热函数外部文件中的更改(在函数上方或下方添加/更改代码)。
-
将函数移动到同一包中的另一个文件(编译器完全忽略源文件名)。
一些可能破坏匹配的更改:
-
热函数内的更改(可能会影响行偏移量)。
-
重命名函数(和/或方法的类型)(更改符号名称)。
-
将函数移动到另一个包(更改符号名称)。
如果配置文件相对较新,则差异可能只影响少数热函数,从而限制了未能匹配的函数中错过优化的影响。尽管如此,随着时间的推移,性能下降会慢慢积累,因为代码很少恢复到旧形式,因此定期收集新配置文件以限制生产环境中的源偏差非常重要。
配置文件匹配可能显著下降的一种情况是,大规模重构会重命名许多函数或将它们在包之间移动。在这种情况下,在新的配置文件显示新结构之前,您可能会经历短期性能下降。
对于例行重命名,理论上可以重写现有配置文件以将旧符号名称更改为新名称。github.com/google/pprof/profile 包含以这种方式重写 pprof 配置文件所需的基本元素,但截至撰写本文时,尚无现成的工具可供使用。
新代码的性能
当添加新代码或通过标志翻转启用新代码路径时,该代码将不会出现在首次构建的配置文件中,因此在收集反映新代码的新配置文件之前,不会获得 PGO 优化。在评估新代码的推出时,请记住,首次发布不代表其稳定状态性能。
常见问题
是否可以使用 PGO 优化 Go 标准库包?
是的。Go 中的 PGO 适用于整个程序。所有包都会重建以考虑潜在的配置文件引导优化,包括标准库包。
是否可以使用 PGO 优化依赖模块中的包?
是的。Go 中的 PGO 适用于整个程序。所有包都会重建以考虑潜在的配置文件引导优化,包括依赖项中的包。这意味着您的应用程序使用依赖项的独特方式会影响应用于该依赖项的优化。
使用不具代表性的配置文件进行 PGO 会不会使我的程序比不使用 PGO 更慢?
不应该。虽然不代表生产行为的配置文件会导致应用程序冷门部分的优化,但它不应该使应用程序热门部分变慢。如果您遇到 PGO 导致性能低于禁用 PGO 的程序,请在 go.dev/issue/new 提交问题。
我可以将同一个配置文件用于不同的 GOOS/GOARCH 构建吗?
是的。配置文件的格式在 OS 和架构配置之间是等效的,因此它们可以用于不同的配置。例如,从 linux/arm64 二进制文件收集的配置文件可以在 windows/amd64 构建中使用。
话虽如此,上面讨论的源代码稳定性注意事项也适用于此处。任何在这些配置之间有所不同的源代码都不会被优化。对于大多数应用程序,绝大多数代码是平台无关的,因此这种形式的性能下降是有限的。
举个具体例子,os
包中文件处理的内部实现在 Linux 和 Windows 之间有所不同。如果这些函数在 Linux 配置文件中是热点,那么 Windows 等效函数将不会获得 PGO 优化,因为它们与配置文件不匹配。
您可以合并不同 GOOS/GOARCH 构建的配置文件。请参阅下一个问题以了解这样做的权衡。
如何处理用于不同工作负载类型的单个二进制文件?
这里没有明显的选择。用于不同类型工作负载的单个二进制文件(例如,在一个服务中以读密集型方式使用的数据库,在另一个服务中以写密集型方式使用的数据库)可能具有不同的热点组件,这些组件受益于不同的优化。
有三个选项:
-
为每个工作负载构建不同版本的二进制文件:使用每个工作负载的配置文件来构建多个工作负载特定的二进制文件版本。这将为每个工作负载提供最佳性能,但可能会增加处理多个二进制文件和配置文件来源的运营复杂性。
-
仅使用“最重要”工作负载的配置文件构建单个二进制文件:选择“最重要”的工作负载(占用空间最大、性能最敏感),并仅使用该工作负载的配置文件进行构建。这将为所选工作负载提供最佳性能,并且由于对跨工作负载共享的通用代码的优化,其他工作负载可能仍会获得适度的性能提升。
-
跨工作负载合并配置文件:从每个工作负载(按总占用空间加权)获取配置文件,并将它们合并为单个“舰队范围”配置文件,用于构建单个通用配置文件。
PGO 如何影响构建时间?
启用 PGO 构建可能会导致包构建时间显着增加。最明显的因素是 PGO 配置文件适用于二进制文件中的所有包,这意味着首次使用配置文件需要重建依赖关系图中的每个包。这些构建像其他任何构建一样都会被缓存,因此后续使用相同配置文件的增量构建不需要完全重建。
如果您遇到构建时间急剧增加的情况,请在 go.dev/issue/new 提交问题。
注意:编译器解析配置文件也可能增加显著的开销,特别是对于大型配置文件。使用大型配置文件和大型依赖关系图会显著增加构建时间。这由 go.dev/issue/58102 跟踪,并将在未来版本中解决。
PGO 如何影响二进制文件大小?
由于额外的函数内联,PGO 可能会导致二进制文件略微增大。
附录:其他配置文件来源
Go 运行时生成的 CPU 配置文件(通过 runtime/pprof 等)已经采用正确的格式,可直接用作 PGO 输入。但是,组织可能有其他首选工具(例如 Linux perf),或者现有的全舰队持续分析系统,他们希望将其与 Go PGO 一起使用。
如果转换为 pprof 格式,则可以使用其他来源的配置文件与 Go PGO,前提是它们遵循以下一般要求:
-
其中一个样本索引应具有类型/单位“samples”/“count”或“cpu”/“nanoseconds”。
-
样本应表示样本位置的 CPU 时间样本。
-
配置文件必须符号化(必须设置 Function.name)。
-
样本必须包含内联函数的堆栈帧。如果省略内联函数,Go 将无法保持迭代稳定性。
-
必须设置 Function.start_line。这是函数起始的行号,即包含
func
关键字的行。Go 编译器使用此字段计算样本的行偏移量(Location.Line.line - Function.start_line
)。请注意,许多现有 pprof 转换器省略了此字段。
注意:在 Go 1.21 之前,DWARF 元数据省略了函数起始行(DW_AT_decl_line
),这可能会使工具难以确定起始行。
有关特定第三方工具的 PGO 兼容性的更多信息,请参阅 Go Wiki 上的 PGO 工具页面。