配置文件引导优化
自 Go 1.20 起,Go 编译器支持配置文件引导优化 (PGO) 以进一步优化构建。
目录:
概述
收集配置文件
使用 PGO 进行构建
注意事项
常见问题
附录:替代配置文件来源
概述
配置文件引导优化 (PGO),也称为反馈指导优化 (FDO),是一种编译器优化技术,它将应用程序代表性运行过程中的信息(一份配置文件)反馈给编译器,用于应用程序的下一次构建,编译器利用这些信息做出更明智的优化决策。例如,编译器可能决定对配置文件中指示为频繁调用的函数进行更积极的内联。
在 Go 中,编译器使用 CPU pprof 配置文件作为 PGO 的输入,例如来自 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。这是一个很好的入门方式,但这种方式可能不具代表性:
- 这个实例在被分析时可能没有做任何事情,即使它通常很忙。
- 流量模式可能在一天中发生变化,导致行为在一天中变化。
- 实例可能执行长时间运行的操作(例如,操作 A 5 分钟,然后操作 B 5 分钟,等等)。一个 30 秒的配置文件可能只覆盖单一类型的操作。
- 实例可能没有公平地接收请求分布(某些实例接收的某种类型的请求比其他实例更多)。更稳健的策略是从不同实例在不同时间点收集多个配置文件,以减少单个实例配置文件差异造成的影响。多个配置文件随后可以被合并为单一配置文件,供PGO(Profile-Guided Optimization)使用。
许多组织运行着"持续性能分析"服务,能够自动执行这种全集群范围的采样式性能分析,这些分析结果即可作为PGO所需的配置文件来源。
合并配置文件
pprof工具可以像这样合并多个配置文件:``` $ go tool pprof -proto a.pprof b.pprof > merged.pprof
## AutoFDO {#autofdo}
Go语言的PGO旨在支持“[AutoFDO](https://research.google/pubs/pub45290/)”风格的工作流程。
让我们回顾[收集配置文件](#collecting-profiles)中描述的工作流程:
1. 构建并发布初始二进制文件(不启用PGO)
2. 从生产环境收集配置文件
3. 当需要发布更新版本时,基于最新源代码构建并使用生产环境配置文件
4. 返回步骤2
这个流程看似简单,但有几个关键特性需要注意:
* 开发持续进行,因此被分析版本(步骤2)的源代码与正在构建的最新源代码(步骤3)可能存在细微差异。Go的PGO设计为对此具有鲁棒性,我们称之为_源代码稳定性_。
* 这是一个闭环流程。即在首次迭代后,被分析的二进制文件已经使用前次迭代的配置文件进行了PGO优化。Go的PGO同样对此具有鲁棒性,我们称之为_迭代稳定性_。
_源代码稳定性_通过启发式算法匹配配置文件中的样本与当前编译源代码。因此,诸如添加新函数等源代码变更通常不会影响现有代码的匹配。当编译器无法匹配变更代码时,部分优化效果会丧失,但这是_优雅降级_的特性。单个函数匹配失败可能导致该函数错失优化机会,但PGO的整体收益通常分散在众多函数中。关于匹配与降级的详细信息,请参见[源代码稳定性](#source-stability)章节。
_迭代稳定性_旨在防止连续PGO构建之间出现性能波动循环(例如:构建1快速,构建2缓慢,构建3快速等)。我们通过CPU配置文件识别热点函数并进行针对性优化。理论上,热点函数可能因PGO优化效果显著,在下一次配置文件中不再显现为热点而停止优化,导致性能回落。Go编译器对PGO优化采取保守策略,我们相信这能有效避免显著性能波动。若您确实观察到此类不稳定现象,请在[go.dev/issue/new](/issue/new)提交问题。
源代码稳定性与迭代稳定性共同消除了对两阶段构建的需求——即不再需要先构建未优化的测试版本(金丝雀发布),再使用PGO进行生产环境构建(除非确实需要极致性能)。
## 源代码稳定性与重构 {#source-stability}
如前所述,Go的PGO会尽力将旧配置文件中的样本持续匹配到当前源代码。具体而言,Go使用函数内的行偏移量(例如:foo函数的第5行调用)。
许多常规变更不会破坏匹配,包括:
* 热点函数外的文件变更(在函数上方或下方添加/修改代码)
* 将函数移动到同一包的其他文件(编译器完全忽略源文件名)
可能破坏匹配的变更包括:
* 热点函数内部的变更(可能影响行偏移量)
* 重命名函数(和/或方法所属类型)(改变符号名称)
* 将函数移动到其他包(改变符号名称)
若配置文件相对较新,差异通常仅影响少量热点函数,从而限制匹配失败函数错失优化机会的影响。但由于代码很少会_回退_到旧形态,性能退化仍会随时间缓慢累积,因此定期收集新配置文件对限制生产环境源码偏差至关重要。
当进行大规模重构(如重命名大量函数或跨包移动函数)时,配置文件匹配度可能显著下降。此时在新配置文件反映新结构前,您可能会经历短期性能下降。
对于机械式重命名,理论上可以重写现有配置文件以更新符号名称。[github.com/google/pprof/profile](https://pkg.go.dev/github.com/google/pprof/profile)包含实现此功能的原语,但截至本文撰写时,尚无现成工具支持此操作。
## 新代码的性能
当通过特性开关添加新代码或启用新代码路径时,首次构建时该代码不会出现在配置文件中,因此在收集到反映新代码的配置文件前不会获得PGO优化。评估新代码发布时请注意:初始版本的性能并不能代表其稳定状态性能。
# 常见问题 {#faq}
## 是否能用PGO优化Go标准库包?
可以。Go的PGO应用于整个程序。所有包都会被重建以考虑潜在的配置文件引导优化,包括标准库包。
## 是否能用PGO优化依赖模块中的包?
可以。Go的PGO应用于整个程序。所有包都会被重建以考虑潜在的配置文件引导优化,包括依赖项中的包。这意味着应用程序使用依赖项的独特方式会影响对该依赖项的优化策略。
## 使用不具代表性的配置文件进行PGO,会导致程序比未启用PGO时更慢吗?**应该不会。**
虽然不具代表性的生产行为配置文件可能导致应用程序非热点部分的优化,但它应该不会使应用程序的热点部分变慢。
如果您遇到启用PGO后性能反而比禁用时更差的程序,请在 [go.dev/issue/new](/issue/new) 提交issue。
## 可以为不同的 GOOS/GOARCH 构建使用相同的配置文件吗?
可以。
配置文件的格式在操作系统和架构配置之间是等效的,因此可以跨不同配置使用。
例如,从 linux/arm64 二进制文件收集的配置文件可以用于 windows/amd64 构建。
尽管如此,[上面](#autofdo)讨论的源代码稳定性注意事项在这里同样适用。
任何在这些配置之间存在差异的源代码都不会被优化。
对于大多数应用程序来说,绝大多数代码是平台无关的,因此这种形式的退化是有限的。
一个具体的例子是,包 `os` 中文件处理的内部实现因 Linux 和 Windows 而异。
如果这些函数在 Linux 配置文件中是热点,那么 Windows 的等效函数将不会获得PGO优化,因为它们与配置文件不匹配。
您可以合并不同 GOOS/GOARCH 构建的配置文件。有关这样做的权衡,请参见下一个问题。
## 如何处理用于不同工作负载类型的单一二进制文件?
这里没有明显的选择。
用于不同类型工作负载的单一二进制文件(例如,一个数据库在一个服务中用于读密集型操作,在另一个服务中用于写密集型操作)可能具有不同的热点组件,这些组件受益于不同的优化。
有三种选择:
1. **为每种工作负载构建不同版本的二进制文件**:使用来自每个工作负载的配置文件来构建多个特定于工作负载的二进制文件。
这将为每种工作负载提供最佳性能,但可能会增加处理多个二进制文件和配置文件来源的运维复杂性。
2. **仅使用来自“最重要”工作负载的配置文件构建单一二进制文件**:选择“最重要”的工作负载(占用资源最多、对性能最敏感),并仅使用该工作负载的配置文件进行构建。
这为所选工作负载提供了最佳性能,并且可能通过优化跨工作负载共享的公共代码,为其他工作负载带来适度的性能提升。
3. **跨工作负载合并配置文件**:获取每个工作负载的配置文件(按总占用资源加权)并将它们合并为一个“全集群范围”的配置文件,用于构建单一的通用配置文件。
这可能会为所有工作负载带来适度的性能提升。
## PGO 如何影响构建时间?
启用PGO构建可能会导致包构建时间出现可测量的增加。
其中最明显的是,PGO配置文件适用于二进制文件中的所有包,这意味着第一次使用配置文件需要重新构建依赖图中的每个包。
这些构建像其他构建一样被缓存,因此使用相同配置文件的后续增量构建不需要完全重建。
如果您遇到构建时间极端增长的情况,请在 [go.dev/issue/new](/issue/new) 提交issue。
## PGO 如何影响二进制文件大小?
由于额外的函数内联,PGO可能导致二进制文件大小略微增加。
# 附录:替代配置文件来源 {#alternative-sources}
Go运行时生成的CPU配置文件(通过 [runtime/pprof](https://pkg.go.dev/runtime/pprof) 等)已经是可直接用作PGO输入的正确格式。
然而,组织可能有其他偏好的工具(例如 Linux perf),或者现有的、希望与Go PGO一起使用的全集群范围持续性能分析系统。
如果转换为 [pprof 格式](https://github.com/google/pprof/tree/main/proto),来自替代来源的配置文件可以与Go PGO一起使用,前提是它们遵循以下一般要求:
* 一个样本索引的类型/单位应为 "samples"/"count" 或 "cpu"/"nanoseconds"。
* 样本应代表在采样位置的CPU时间采样。
* 配置文件必须是已符号化的([Function.name](https://github.com/google/pprof/blob/76d1ae5aea2b3f738f2058d17533b747a1a5cd01/proto/profile.proto#L208) 必须设置)。
* 样本必须包含内联函数的栈帧。
如果内联函数被省略,Go将无法维持迭代稳定性。
* [Function.start_line](https://github.com/google/pprof/blob/76d1ae5aea2b3f738f2058d17533b747a1a5cd01/proto/profile.proto#L215) 必须设置。
这是函数起始行的行号。
即包含 `func` 关键字的那一行。
Go编译器使用此字段来计算样本的行偏移量(`Location.Line.line - Function.start_line`)。
**请注意,许多现有的pprof转换器会省略此字段。**
_注意:在 Go 1.21 之前,DWARF 元数据会省略函数起始行(`DW_AT_decl_line`),这可能使工具难以确定起始行。_
有关特定第三方工具的PGO兼容性更多信息,请参阅Go Wiki上的 [PGO工具](/wiki/PGO-Tools) 页面。