PGO 是啥, 咋就让 Go 更快更猛了?
Go1.20 即将发布, 近期很多大佬提到一个关键词 PGO, 说是有很大的提高, 很猛。… 称其为本迭代(Go1.20)最大的功能特色。
PGO 是什么
Profile-guided optimization (PGO), 翻译过来是使用配置文件引导的优化。也被称为:
- profile-directed feedback(PDF)
- feedback-directed optimization(FDO)
PGO 是计算机编程中的一种编译器优化技术, 借助配置文件来引导编译, 达到提高程序运行时性能的目的。
该项优化是一个通用技术, 不局限于某一门语言。像是我们常见的:
- Chrome 浏览器, 在 64 位版本的 Chrome 中从 53 版开始启用 PGO, 32 位版在 54 版中启用。
- Microsoft Visual C++ 也同样有所使用。
- AutoFDO 进行了 PGO 的优化, 直接将某数据中心中的 C/C++ 程序的性能提高了 5-15%(不用改业务代码)。
这个优化成绩, 一听就很振奋人心。
PGO 怎么优化
《Intel Developer Guide and Reference》中对 PGO 的优化和流程有一个基本介绍, 如下内容, 分享给大家。
PGO 通过缩小代码大小、减少代码分支错误预测和重新组织代码布局以减少指令缓存问题来提高应用程序性能。也可以向编译器提供有关应用程序中最常执行的区域的信息。
编译器再通过分析这些信息, 编译器能够在优化应用程序时更具选择性和针对性, 做出最优的选择。
PGO 由三个阶段组成。如下图:
- 检测程序。编译器从你的源代码和编译器的代码创建并链接一个检测程序。
- 运行检测的可执行文件。每次执行插桩代码时, 插桩程序都会生成一个动态信息文件, 用于最终编译。
- 最终编译。当您第二次编译时, 动态信息文件将合并到一个摘要文件中。编译器会使用此文件中的概要信息, 然后尝试选择程序中最频繁最优的运行路径去执行。
这就是 PGO 这项优化的基本过程了。
新提案
背景
提案作者(Cherry Mui、Austin Clements、Michael Pratt)建议向 Go GC 工具链增加对配置文件引导优化 (PGO) 的支持。
可以使得 Go tool(工具链)能根据运行时信息执行特定于应用程序和工作负载的优化。说明了就是想提高性能, 不改业务代码。
用什么来做
PGO 需要用户参与来收集配置文件并将其反馈到构建过程中才能优化, 这个前置条件是一个大问题。最符合这个要求的, 就是 pprof
。
一番讨论后, Go 团队最终也敲定将基于 runtime/pprof
来得到所需 profile
, 以此来完成 PGO。因为它符合: 采集样本开销低、多系统兼容性强、Go 标准且被广泛使用的基准。
也就是有 runtime/pprof 生成的 profile, 就能搞 PGO 了!
支持到什么程度
PGO 第一个版本将会先支持 pprof CPU, 直接读取 pprof CPU profile 文件来完成优化。预计将在 Go1.20 发布预览版本。
在 Go 工具链上, 将在 go build 子命令增加 -pgo=<path>
, 用于显式指定用于 PGO 构建的 profile
文件位置。
可能会有同学说, 还得显式指定, 太麻烦了? 这 Go 团队也考虑到了。…
只需要你将其设置为: -pgo=auto
, 就会自动去读取主目录下的 profile
文件, 非常香!
如果不需要, 那就直接 -pgo=off
就能完全关闭 PGO。
Go1.20 实现 PGO 的预览版本, 配置默认为 off
, 成熟后会默认为 auto
。
从哪里先动手
Go 团队先会专注于 Go 编译器的开发, 毕竟这是万物的开始, 后续会在 cmd/go
做一些简单的支持。PGO 第一个动手的方向是: 函数内联。这项被认为性价比是最高的。
未来展望上, 还会包含: devirtualization(去虚拟化, 一种编译器优化策略)、特定泛型函数的模板化、基本块排序和函数布局。
甚至后续会用于改进内存行为, 例如: 改进逃逸行为和内存分配。
To add to what others have said, we’ve actually been keeping an eye toward many possible PGO-based optimizations while designing this. Here’s a non-exhaustive list:
- Inlining just seems like the obvious first step, since it’s likely to provide a lot of bang for your buck. We’ve been talking about using PGO for inlining for many, many years. 😃
- Local basic block ordering: ordering blocks within a function to cluster hot blocks and potentially improve branch prediction (though the win of the latter on modern CPUs is small).
- Register allocation: register allocation currently uses heuristics to determine a hot path and move spills off that hot path. PGO can tell it the true hot path.
- Function ordering: Clustering functions at a whole-binary level for better locality.
- Global block ordering: A step beyond function ordering. The basic form of this is hot/cold splitting, but it can be more aggressive than that.
- Indirect call devirtualization: If the profile shows that a particular function is by far the most common target of an indirect call, specialize the caller to check that that’s the target and make a direct call if it is (possibly even inlining the target). (Edit: Note that this would apply to both closure calls and interface method calls.)
- Stenciling: Stencil hot generic functions, possibly based on type information from the profile.
- Map/slice pre-sizing: Pre-size maps and slices based on allocation site. (This requires more than a CPU profile.)
- Lifetime allocation: Co-locate allocations with similar lifetimes by allocation site. (This also requires more than a CPU profile.)
看看这个 PGO 的未来展望, 这个饼, 我感觉画的又大又圆(远)…
超前实践
以下来自 @Frederic Branczyk 在《Exploring Go’s Profile-Guided Optimizations》一文中, 提前使用 PGO 对 Go 官方已经开发的函数内联进行了提前尝鲜。
步骤如下:
首先拉取已实现的 Go 源码并进行编译和导入。如下代码:
git clone https://go.googlesource.com/go
cd go
git fetch https://go.googlesource.com/go refs/changes/63/429863/3 && git checkout -b change-429863 FETCH_HEAD
cd src
./all.bash
cd ..
export PATH="$(pwd)/bin:$PATH" # or add the path to your bashrc/zshrc
进入到 PGO 的内联测试代码:
cd src/cmd/compile/internal/test/testdata/pgo/inline
做提前准备, 生成 pprof cpu profile 文件:
go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof
完成准备动作后。我们进行两次测试: 一次不用 PGO, 一次用 PGO, 来进行对比。
不使用 PGO 的情况:
go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m" 2>&1 | grep "can inline"
./inline_hot.go:15:6: can inline D with cost 7 as: func(uint) int { return int((i + (wSize - 1)) >> lWSize) }
./inline_hot.go:19:6: can inline N with cost 20 as: func(uint) *BS { bs = &BS{...}; return bs }
...
使用 PGO 的情况:
go test -run=none -tags='' -timeout=9m0s -gcflags="-m -m -pgoprofile inline_hot.pprof"
用于如下对比:
go test -o inline_hot.test -bench=. -cpuprofile inline_hot.pprof -count=100 > without_pgo.txt
go test -o inline_hot.test -bench=. -gcflags="-pgoprofile inline_hot.pprof" -count=100 > with_pgo.txt
benchstat without_pgo.txt with_pgo.txt
name old time/op new time/op delta
A-10 960µs ± 2% 950µs ± 1% -1.05% (p=0.000 n=98+83)
从结论来看, 引入 PGO 后有了 1%
的性能改进。当然, 这只是一小段测试代码。不同的程序结果会不一样。
总结
PGO 是一门编译器优化技术, 能够在不改业务代码的情况下, 给你的应用程序带来一定的性能提升。在 Go PGO 中将会依托 runtime/pprof
所生成的 profile
来完成(需改造), 也算是做了一个不错的串联。
另外从需求出发点来看, 这项优化感觉更多的来自开发同学的兴趣优化, 官方 issues 中并没有指出是由于什么用户痛点导致的要去开发这项功能。
不过后续如果遇到一些需要进一步优化的 Go 程序, PGO 将会是一个不错的选择。毕竟不用改业务代码。