Go 1.20: Profile-Guided Optimization
ついにGo 1.20がリリースされましたね。
様々なアップデートが含まれているため、Release Noteを読んだりフューチャーさんのGo.1.20の連載などで改善点を確認すると楽しいです。
Go 1.20 All You Need to Know
https://miro.com/app/board/uXjVPyMixKs=/
この記事では、その中でも面白かったProfile-Guided Optimization(PGO)に関してご紹介したいと思います。
PGOって何?
Profile-Guided Optimizationは、コンパイル時最適化のヒントとして、ランタイムのプロファイル情報を使うことでより効率よく最適化する手法です。
静的解析に基づく最適化は、あくまでコード上の特徴やその内部表現(SSA)を解析し、一定の条件を満たしていれば最適化を行う投機的なものでしたが、PGOを使った場合は更に実際に実行した際の特徴をフィードバックし最適化することができ、(うまくプロファイルデータを集められれば)より効率よく最適化を行うことができるといったものです。
C++やRustなどで触ったことがある人もいらっしゃるかもしれません。
この機能のプレビュー版がGo 1.20でコンパイラに導入されました。
We’re particularly excited to launch a preview of profile-guided optimization (PGO), which enables the compiler to perform application- and workload-specific optimizations based on run-time profile information. Providing a profile to go build enables the compiler to speed up typical applications by around 3–4%, and we expect future releases to benefit even more from PGO. Since this is a preview release of PGO support, we encourage folks to try it out, but there are still rough edges which may preclude production use.
https://go.dev/blog/go1.20
PGO-based inlining
Go 1.20では、まず第一弾としてプロファイルデータを基にしたインライン最適化(PGO-based inlining)が導入されました。
(あくまでプレビュー版という位置づけで、今後正式リリースとなるようです)
インライン最適化は呼び出している関数の中身を、呼び出し元の関数内に展開することで関数呼び出しにまつわる様々なコストを削減する最適化です。ホットパスだと結構パフォーマンスに効いてきます。
インライン最適化自体は太古からずっと存在し、その内容も年々改善されてきました。
Go 1.5以降では従来の関数内が40ノードという制限が80ノードまで緩和され、transitive inliningが実装(https://github.com/golang/go/commit/77ccb16eb12f461eaea5fdf652a2e929dc154192)、
Go 1.9以降ではmid-stack inlining、Go 1.16ではラベルを含まないforループのインライン化、Go 1.18ではラベルを含むfor, switch文のインライン化などなど、様々な工夫によって徐々に制限が緩まってきました。
今回、PGO-based inliningによって80ノードを超える場合でも、プロファイルデータを基に最適化の価値があると判断された場合、インライン化されるようになりました。(https://github.com/golang/go/blob/2da8a55584aa65ce1b67431bb8ecebf66229d462/src/cmd/compile/internal/inline/inl.go#L310)
今後の発展も
今回のリリースではPGO-based inliningのみでしたが、IssueではPGOを用いた様々な最適化が議論されていました。(escape analysis, devirtualization, 局所性の改善, レジスタ割り付け, GenericsのStencil化, Map/Sliceのcap予測...)
PGOが入ることにより、今後様々な最適化の可能性が開けそうです。
継続的プロファイルフロー: AutoFDO
ただし、肝心のプロファイルデータがマイクロベンチマークによるものであったり、実際のワークロードとは大きく違うものでは改善が見られない可能性があります。
For best results, it is important that profiles are representative of actual behavior in the application’s production environment. Using an unrepresentative profile is likely to result in a binary with little to no improvement in production. Thus, collecting profiles directly from the production environment is recommended, and is the primary method that Go’s PGO is designed for.
https://go.dev/doc/pgo
公式ドキュメントで推奨されている方法は以下です:
- PGOなしのバイナリをリリースする
- 継続的プロファイリングサービスなどにより、プロダクション環境のプロファイルデータを収集する
- プロファイル情報を使って最新のソースコードをPGOありでビルドし、リリースする
- 2に戻る
フローがループしていますね。このフローはAutoFDOと呼ばれているらしく、Google内で実績があるそうです。
しかし、このフローでは古いソースコードのプロファイルデータを、最新のソースコードに適用することになります。可能なのでしょうか?
安定性
公式ドキュメントでは、GoのPGO実装の特徴として二つの安定性が挙げられています:
- ソース安定性: 少し古いバージョンのソースコードで取得されたプロファイルデータを、最新のソースコードに使うことができる
- 反復安定性: PGOありでビルドされたバイナリのプロファイルデータを、次のPGOに使うことができる
これらの安定性により、継続的にPGOし続けるシンプルなデプロイフローを構築できるとしています。
(ただし、大規模なリファクタやリネーム、ホットパスの修正などが入ると、プロファイルデータとのマッチングに失敗し一時的なパフォーマンス低下が起こるそうです)
その他、詳しい使用方法や注意点は公式ドキュメントに記載がありますので、確認してみてください。
また、こちらの記事 Exploring Go's Profile-Guided Optimizations では実際にインライン最適化が行われている様子が紹介されていて面白いです。
最後に
AutoFDOまで組み込めると、今後のPGO周りの最適化の恩恵を常に受けることができ、パフォーマンス向上に効果がありそうです。
以上、Go 1.20で実装されたPGOのご紹介でした。
参考文献
- Exploring Go's Profile-Guided Optimizations, https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/
- Profile-guided optimization, https://go.dev/doc/pgo
- Generics can make your Go code slower, https://planetscale.com/blog/generics-can-make-your-go-code-slower
- Golangのメモリ周りのメモ (mid-stack inlining編) - MJHD, https://mjhd.hatenablog.com/entry/2019/05/20/132111
- Golangのメモリ周りのメモ - MJHD, https://mjhd.hatenablog.com/entry/2018/09/30/205921
Discussion