基于配置文件的优化
从 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 构建使用相同的配置文件吗?
可以。配置文件的格式在不同的操作系统和架构配置中是等效的,因此它们可以在不同的配置中使用。例如,从 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,前提是它们符合以下一般要求
-
其中一个样本索引应具有类型/单位“样本”/“计数”或“cpu”/“纳秒”。
-
样本应表示样本位置的 CPU 时间样本。
-
配置文件必须带符号(Function.name 必须设置)。
-
样本必须包含内联函数的堆栈帧。如果省略内联函数,Go 将无法保持迭代稳定性。
-
Function.start_line 必须设置。这是函数开始处的行号。即包含
func
关键字的行。Go 编译器使用此字段计算样本的行偏移(Location.Line.line - Function.start_line
)。请注意,许多现有的 pprof 转换器会省略此字段。
注意:在 Go 1.21 之前,DWARF 元数据会省略函数开始行(DW_AT_decl_line
),这可能会给工具确定开始行带来困难。
请参阅 Go Wiki 上的 PGO 工具 页面,以获取有关特定第三方工具的 PGO 兼容性的其他信息。