https://inasa.dev/blog/rss.xml

Go | PGO翻译

2023-06-30

Go 1.20开始,Go编译器支持配置文件引导的优化(PGO),以进一步优化构建。

Overview

Profile-guided optimization(配置文件指导下的优化)(PGO),也被称为 feedback-directed optimization (反馈指导下的优化)(FDO),是一种编译器优化技术,它将应用程序的代表性运行信息(配置文件)反馈给编译器,用于应用程序的下一次构建,编译器利用这些信息做出更明智的优化决定。例如,编译器可以决定更积极地内联那些概况显示经常被调用的函数。

Go中,编译器使用CPU pprof配置文件作为输入配置文件,例如来自runtime/pprofnet/http/pprof

截至Go 1.21,一组有代表性的Go程序的基准测试显示,使用PGO构建程序可以提高约2-7%的性能。我们预计,随着更多的优化措施在未来的Go版本中利用PGO,性能的提高会随着时间的推移而普遍增加。

Collecting profiles

Go编译器希望CPU pprof配置文件作为PGO的输入。Go运行时生成的配置文件(例如来自运行时runtime/pprofnet/http/pprof)可以直接作为编译器的输入。也可以使用/转换其他剖析系统的配置文件。

为了获得最佳效果,配置文件必须代表应用程序生产环境中的实际行为。使用不具代表性的配置文件可能会导致二进制文件在生产中几乎没有任何改进。因此,建议直接从生产环境中收集配置文件,这也是 GoPGO 所设计的主要方法。

典型的工作流程如下:

  1. 构建并发布初始二进制文件(不含 PGO)
  2. 从生产中收集配置文件
  3. 当需要发布更新的二进制文件时,从最新的源代码构建并提供生产配置文件
  4. GOTO 2

Go PGO 对于应用程序的剖析版本和使用剖析文件构建的版本之间的偏差,以及使用从已优化的二进制文件中收集的剖析文件进行构建,一般都很稳健。这也是使这个迭代生命周期成为可能的原因。关于这个工作流程的其他细节,请参见AutoFDO部分。

Building with PGO

标准的构建方法是将文件名为default.pgopprof CPU配置文件存储在被剖析二进制文件的主包目录中。默认情况下,go build会自动检测default.pgo文件并启用PGO

推荐在源码库中直接提交配置文件,因为配置文件是构建的重要输入,对于可重复性(和性能!)的构建非常重要。与源码一起存储可以简化构建体验,因为除了获取源码之外,没有其他步骤可以获得配置文件。

对于更复杂的情况,go build -pgo标志控制PGO配置文件的选择。这个标志的默认值是-pgo=auto,即上述的默认.pgo行为。将该标志设置为-pgo=off则完全禁用PGO优化。

如果你不能使用default.pgo(例如,一个二进制文件的不同场景需要不同的配置文件,无法将配置文件与源代码一起存储,等等),你可以直接传递一个要使用的配置文件的路径(例如,go build -pgo=/tmp/foo.prof)。

例如,go build -pgo=/tmp/foo.prof ./cmd/foo ./cmd/barfoo.prof应用于二进制文件foobar,这往往不是你想要的。通常,不同的二进制文件应该有不同的配置文件,通过单独的go build调用传递。

注意:在Go 1.21之前,默认是-pgo=off。必须明确启用PGO

Notes

从生产中收集代表性资料

你的生产环境是你的应用程序有代表性的配置文件的最佳来源,如收集配置文件中所述。

最简单的方法是将 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”风格的工作流程。

让我们仔细看看收集资料中描述的工作流程:

  1. 构建并发布初始二进制文件(不含 PGO
  2. 从生产中收集配置文件
  3. 当需要发布更新的二进制文件时,从最新的源代码构建并提供生产配置文件
  4. GOTO 2

这听起来很简单,但这里有几个重要的属性需要注意:

  • 开发始终是持续的,所以二进制的剖析版本的源代码(步骤2)可能与正在构建的最新源代码(步骤3)略有不同。Go PGO的设计是为了适应这种情况,我们称之为源代码的稳定性。
  • 这是一个闭环。也就是说,在第一次迭代之后,二进制文件的剖析版本已经通过前一次迭代的剖析进行了PGO优化。Go PGO也被设计为对这种情况具有鲁棒性,我们称之为迭代稳定性。

源稳定性和重构

如上所述,GoPGO尽最大努力尝试继续将旧的配置文件中的样本与当前的源代码进行匹配。具体来说,Go使用函数内的行偏移量(例如,在函数foo的第5行调用)。

许多常见的更改不会破坏匹配,包括:

  • 在热函数之外更改文件(在函数上方或下方添加/更改代码)
  • 将函数移动到同一包中的另一个文件(编译器完全忽略源文件名)

一些可能会破坏匹配的更改:

  • 热函数内的更改(可能会影响行偏移)
  • 重命名函数(和/或方法的类型)(更改符号名称)
  • 将函数移动到另一个包(更改符号名称)

新代码的性能

当添加新的代码或通过翻转标志启用新的代码路径时,该代码在第一次构建时不会出现在配置文件中,因此在收集到反映新代码的新配置文件之前,不会得到PGO优化。在评估新代码的推出时,请记住,初始版本并不代表其稳定状态的性能。


  • https://go.dev/doc/pgo
  • https://oilbeater.com/2023/06/24/optimization-without-changing-code-pgo/