聊聊Go1.20即将引入的PGO
@ 归零 | 星期一,十月 3 日,2022 年 | 1 分钟阅读 | 更新于 星期一,十月 3 日,2022 年

PGO 是什么?

PGO(profile-guided optimization) 配置文件引导的优化,也被称为反馈驱动的优化(FDO),是一种强大的优化技术,它使用程序运行时行为的配置文件来指导编译器对该程序未来构建的优化。这种技术也可以应用于其他构建阶段,如源代码生成、链接时间或链接后时间(如LTO、BOLT、Propeller),甚至是运行时间。

与传统的、基于启发式的优化相比,PGO有几个优势。许多编译器的优化是有取舍的:例如,内联通过减少调用开销和启用其他优化来提高性能;但内联也会增加二进制大小,从而增加 I-cache 的压力,所以过多的内联会损害整体性能。优化启发式方法旨在平衡这些权衡,但对于任何特定应用程序的峰值性能来说,很少能实现正确的平衡。使用在运行时收集的配置文件数据,编译器拥有不可能静态得出的信息,因为它取决于应用程序的工作负载,或者在合理的编译时间预算内计算的成本太高。有时,用户会求助于源代码级的编译器指令,如 “内联 “指令来指导优化工作。然而,源代码级的编译器指令也不是在所有情况下都能很好地工作。例如,一个库的作者想把那些对该库的性能很重要的函数标记为内联。但是对于一个程序来说,这个库的性能可能并不重要。如果我们把所有库中的重要函数都内联,就会导致构建速度慢、二进制大小膨胀,也许运行时性能也会变慢。另一方面,PGO可以利用整个程序的行为信息,只对程序中对性能至关重要的部分进行优化。

实现

profile文件来源和格式

最初将支持pprof CPU配置文件。开发者可以通过通常的方式来收集配置文件,例如 runtime/pprof 或 net/http/pprof 包。编译器将直接读取pprof CPU配置文件。在未来,我们可能会支持更多类型的配置文件

go命令的变化

  • go命令将在主包的源代码目录中搜索名为default.pgo的配置文件,如果存在的话,将为主包的横向依赖中的所有包提供该配置文件。如果go命令在任何非主包目录下发现default.pgo,它将报告一个错误。在未来,可能会支持为不同的构建配置(如GOOS/GOARCH)自动查找不同的配置文件,但目前我们希望配置文件在不同的配置之间是相当可移植的。

  • 为go build添加一个-pgo=命令行标志,指定一个明确的profile文件位置,用于PGO的构建。该标志在以下情况下很有用

    • 一个具有相同源代码的程序有多个用例,有不同的配置文件
    • 构建配置对profile文件有很大影响
    • 用不同的profile文件进行测试
    • 即使有一个配置文件,也要禁用PGO 具体来说,-pgo=将选择路径上的prefile文件,-pgo=auto将选择主包源码目录下的profile文件;-pgo=off将完全关闭PGO,即使主包源码目录下有一个配置文件存在。

在Go 1.20中默认为关闭,PGO不会被启用。在未来的版本中,默认为自动。

为了确保构建是可重现的,profile文件的内容将被视为构建的输入,并将被纳入构建缓存密钥计算和BuildInfo。 主软件包的 go test 将使用与 go build 相同的profile文件搜索规则。

编译器的变化

将修改编译器以支持读取从go命令中传递过来的pprof配置文件,并修改其优化启发式方法以使用这些配置文件信息。这不需要一个新的API。实施细节不包括在本提案中。最初,go team计划增加基于PGO的内联优化。未来将增加更多的优化,如:devirtualization、特定通用函数的模板化、基本块排序和函数布局等。

PGO内联优化

在计算机科学中,内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。

注意: 内联优化一般用于能够快速执行的函数,因为在这种情况下函数调用的时间消耗显得更为突出,同时内联体量小的函数也不会明显增加编译后的执行文件占用的空间。

PGO 的内联决策基于调用图路径分析。简而言之,使用调用图路径分析进行内联决策背后的基本原则是了解从特定调用路径调用的函数的行为。这很重要,因为来自一个调用路径的函数调用行为可能与另一个调用路径截然不同。了解哪个调用路径更热有助于更好地内联决策,因为优化器仅内联频繁调用路径,从而最大限度地减少内联导致的代码膨胀,但仍通过内联更热的调用路径获得性能。看看下面给出的例子:

从“goo”、“foo”和“bat”调用函数“bar”的顺序

上面中描述的示例说明了从函数“goo”、“foo”和“bat”对函数“bar”进行的函数调用。边缘的数字表示分别从函数“goo”、“foo”和“bat”调用函数“bar”的次数。因此,从函数“goo”到函数“bar”的边表示对于给定的 PGO 培训课程,函数“bar”被函数“goo”调用了 10 次。现在调用图路径分析就是从特定调用路径中找出函数调用的行为。因此,图 1a 将进一步分解为图 1b。

从“goo”、“foo”和“bat”调用函数“bar”的序列 从“bar”调用函数“baz”的序列

分析上图, 很明显,考虑到从 ‘bat’ 到 ‘bar’ 的调用频率更高,即 100 次调用,主要好处将来自内联函数 ‘bar’ 到 ‘bat’。此外,考虑到从调用路径“goo-bar”和“foo-bar”对函数“baz”的高频率调用,另一个主要优势将来自内联“baz”到“bar”中也很明显。pgo-inlining 对上述场景的影响如下图所示。

PGO 内联的影响,“bar”被内联到“bat”,“baz”被内联到“bar”

内联决策是在布局、速度与大小决策以及所有其他优化之前做出的。

未来,Go会添加更多基于PGO的优化,例如Devirtualization,特定通用功能的刻板,基本块排序和功能布局。同时还考虑使用PGO来改善内存行为,例如改进逃生分析和内存分配。以往真实场景使用 PGO 的经验表明,使用PGO优化,程序的性能可以提高 5-15%,而且不用修改一行代码,因此我们对使用 PGO 的 Go 的未来感到兴奋,让我们拭目以待吧。

参考链接:

  1. https://go.googlesource.com/proposal/+/master/design/55022-pgo.md

  2. https://github.com/golang/go/issues/55022

© 2014 - 2022 Lionel's Blog

Powered by Hugo with theme Dream.