プロファイルガイド最適化

Go 1.20以降、Goコンパイラはビルドをさらに最適化するためにプロファイルガイド最適化 (PGO) をサポートしています。

目次

概要
プロファイルの収集
PGOを使用したビルド

よくある質問
付録: プロファイル代替ソース

概要

プロファイルガイド最適化 (PGO) は、フィードバック指向最適化 (FDO) とも呼ばれ、アプリケーションの代表的な実行からの情報 (プロファイル) を、アプリケーションの次のビルドのためにコンパイラにフィードバックするコンパイラ最適化手法です。コンパイラは、その情報を使用して、より情報に基づいた最適化の決定を行います。たとえば、コンパイラは、プロファイルが頻繁に呼び出されていると示す関数をより積極的にインライン化することを決定する可能性があります。

Goでは、コンパイラは、runtime/pprofnet/http/pprofなどのCPU pprofプロファイルを入力プロファイルとして使用します。

Go 1.22の時点で、代表的なGoプログラムのベンチマークでは、PGOでビルドするとパフォーマンスが約2〜14%向上することが示されています。今後のGoバージョンでPGOを利用した最適化が追加されるにつれて、パフォーマンスの向上が一般的に増加すると予想されます。

プロファイルの収集

Goコンパイラは、PGOへの入力としてCPU pprofプロファイルを期待します。Goランタイムによって生成されたプロファイル (たとえば、runtime/pprofnet/http/pprofなど) は、コンパイラ入力として直接使用できます。他のプロファイリングシステムからのプロファイルを使用/変換することもできます。詳細については、付録を参照してください。

最良の結果を得るには、プロファイルがアプリケーションの運用環境での実際の動作を代表していることが重要です。代表的でないプロファイルを使用すると、運用環境での改善がほとんどまたはまったくないバイナリになる可能性が高くなります。したがって、運用環境から直接プロファイルを収集することをお勧めします。これはGoのPGOが設計されている主な方法です。

一般的なワークフローは次のとおりです

  1. (PGOなしで)初期バイナリをビルドしてリリースします。
  2. 運用環境からプロファイルを収集します。
  3. 更新されたバイナリをリリースする際には、最新のソースからビルドし、運用プロファイルを提供します。
  4. GOTO 2

Go PGOは、プロファイリングされたバージョンのアプリケーションとプロファイルでビルドするバージョンとの間のスキュー、およびすでに最適化されたバイナリから収集されたプロファイルを使用したビルドに対して、一般的に堅牢です。これにより、この反復的なライフサイクルが可能になります。このワークフローの詳細については、AutoFDOセクションを参照してください。

運用環境から収集することが困難または不可能な場合 (たとえば、エンドユーザーに配布されるコマンドラインツール)、代表的なベンチマークから収集することもできます。代表的なベンチマークを構築することは非常に困難であることに注意してください (アプリケーションが進化するにつれて代表的な状態を維持することも同様です)。特に、マイクロベンチマークは通常、PGOプロファイリングには適していません。アプリケーションの小さな部分しか実行しないため、プログラム全体に適用した場合に得られるゲインが小さくなります。

PGOを使用したビルド

ビルドの標準的なアプローチは、プロファイリングされたバイナリのメインパッケージディレクトリにファイル名default.pgoのpprof CPUプロファイルを保存することです。デフォルトでは、go builddefault.pgoファイルを自動的に検出し、PGOを有効にします。

プロファイルは再現可能 (かつ高性能!) なビルドにとって重要なビルドへの入力であるため、ソースリポジトリに直接プロファイルをコミットすることをお勧めします。ソースと一緒に保存すると、ソースをフェッチする以外にプロファイルを取得するための追加手順がないため、ビルドエクスペリエンスが簡素化されます。

より複雑なシナリオでは、go build -pgoフラグがPGOプロファイルの選択を制御します。このフラグは、上記で説明したdefault.pgoの動作の場合、デフォルトで-pgo=autoになります。フラグを-pgo=offに設定すると、PGO最適化が完全に無効になります。

default.pgoを使用できない場合 (たとえば、1つのバイナリの異なるシナリオに対して異なるプロファイルを使用する場合、ソースとともにプロファイルを保存できない場合など)、使用するプロファイルのパスを直接渡すことができます (たとえば、go build -pgo=/tmp/foo.pprof)。

注: -pgoに渡されたパスは、すべてのメインパッケージに適用されます。たとえば、go build -pgo=/tmp/foo.pprof ./cmd/foo ./cmd/barは、両方のバイナリfoobarfoo.pprofを適用しますが、これは多くの場合、望ましくありません。通常、異なるバイナリは異なるプロファイルを持つ必要があり、個別のgo build呼び出しで渡されます。

注: Go 1.21より前では、デフォルトは-pgo=offです。PGOを明示的に有効にする必要があります。

運用環境からの代表的なプロファイルの収集

プロファイルの収集で説明したように、運用環境はアプリケーションの代表的なプロファイルの最適なソースです。

これを開始する最も簡単な方法は、net/http/pprofをアプリケーションに追加し、サービスの任意のインスタンスから/debug/pprof/profile?seconds=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

これは一見すると非常に単純に聞こえますが、ここに注意すべきいくつかの重要なプロパティがあります。

ソース安定性は、プロファイルからのサンプルをコンパイルソースに一致させるためのヒューリスティックを使用することで実現されます。その結果、新しい関数の追加など、ソースコードに対する多くの変更は、既存のコードのマッチングに影響を与えません。コンパイラが変更されたコードを照合できない場合、一部の最適化は失われますが、これはグレースフルデグラデーションであることに注意してください。単一の関数が照合に失敗すると、最適化の機会を逃す可能性がありますが、PGOの利点は通常、多くの関数に分散されます。マッチングと劣化の詳細については、ソース安定性セクションを参照してください。

反復安定性は、連続するPGOビルドでの可変パフォーマンスのサイクルを防ぐことです (たとえば、ビルド#1は高速、ビルド#2は低速、ビルド#3は高速など)。CPUプロファイルを使用して、最適化のターゲットとなるホット関数を識別します。理論的には、PGOによってホット関数が大幅に高速化され、次のプロファイルでホットと見なされなくなり、最適化されず、再び遅くなる可能性があります。GoコンパイラはPGO最適化に対して保守的なアプローチを採用しており、これにより大きな分散を防げると考えています。この種の不安定性が観察された場合は、go.dev/issue/newで問題を提出してください。

ソース安定性と反復安定性を組み合わせることで、最初の最適化されていないビルドがカナリアとしてプロファイリングされ、次にPGOを使用して運用環境用に再ビルドされる、2段階のビルドの要件を排除します (絶対にピークパフォーマンスが必要な場合を除きます)。

ソース安定性とリファクタリング

上記で説明したように、GoのPGOは、古いプロファイルからのサンプルを現在のソースコードに照合し続けるために最善の努力をします。具体的には、Goは関数内の行オフセット (たとえば、関数fooの5行目の呼び出し) を使用します。

次のような多くの一般的な変更は、マッチングを中断しません。

マッチングを中断する可能性のある変更。

プロファイルが比較的最近のものである場合、差異は少数のホット関数にのみ影響を与える可能性が高く、一致に失敗した関数での最適化の損失の影響を制限します。それでも、コードが以前の形式に戻ってリファクタリングされることはめったにないため、時間の経過とともに劣化がゆっくりと蓄積するため、運用環境からのソーススキューを制限するために新しいプロファイルを定期的に収集することが重要です。

プロファイルマッチングが著しく低下する可能性がある状況の一つに、多数の関数の名前変更やパッケージ間での移動を伴う大規模なリファクタリングがあります。この場合、新しいプロファイルが新しい構造を示すまで、短期的なパフォーマンスの低下が生じる可能性があります。

単純な名前変更の場合、既存のプロファイルを書き換えて、古いシンボル名を新しい名前に変更することが理論的には可能です。github.com/google/pprof/profile には、この方法でpprofプロファイルを書き換えるために必要なプリミティブが含まれていますが、執筆時点では、このための既製ツールは存在しません。

新しいコードのパフォーマンス

新しいコードを追加したり、フラグを切り替えて新しいコードパスを有効にしたりする場合、そのコードは最初のビルド時のプロファイルには存在しないため、新しいコードを反映した新しいプロファイルが収集されるまで、PGO最適化は適用されません。新しいコードのロールアウトを評価する際には、初期リリースがその定常状態のパフォーマンスを表していないことを念頭に置いてください。

よくある質問

Go標準ライブラリのパッケージをPGOで最適化することは可能ですか?

はい。GoのPGOはプログラム全体に適用されます。すべてのパッケージが、標準ライブラリのパッケージを含め、プロファイルに基づいた最適化の可能性を考慮して再構築されます。

依存モジュールのパッケージをPGOで最適化することは可能ですか?

はい。GoのPGOはプログラム全体に適用されます。すべてのパッケージが、依存関係にあるパッケージを含め、プロファイルに基づいた最適化の可能性を考慮して再構築されます。これは、アプリケーションが依存関係をどのように使用するかが、その依存関係に適用される最適化に影響を与えることを意味します。

代表的でないプロファイルを使用したPGOは、PGOなしの場合よりもプログラムを遅くしますか?

そうなるべきではありません。本番環境の動作を代表しないプロファイルでは、アプリケーションのコールドな部分が最適化されることになりますが、ホットな部分が遅くなることはないはずです。PGOによってPGOを無効にするよりもパフォーマンスが悪くなるプログラムが発生した場合は、go.dev/issue/newで問題を報告してください。

異なるGOOS/GOARCHビルドで同じプロファイルを使用できますか?

はい。プロファイルの形式はOSおよびアーキテクチャ構成間で同等であるため、異なる構成間で使用できます。たとえば、linux/arm64バイナリから収集されたプロファイルをwindows/amd64ビルドで使用できます。

ただし、上記で説明したソースの安定性に関する注意点もここにも適用されます。これらの構成間で異なるソースコードは最適化されません。ほとんどのアプリケーションでは、コードの大部分がプラットフォームに依存しないため、この形式の劣化は限定的です。

具体的な例として、osパッケージのファイル処理の内部は、LinuxとWindowsで異なります。これらの関数がLinuxプロファイルでホットな場合、Windowsの同等の関数はプロファイルと一致しないため、PGO最適化は適用されません。

異なるGOOS/GOARCHビルドのプロファイルをマージできます。それを行う場合のトレードオフについては、次の質問を参照してください。

異なるワークロードタイプに使用される単一のバイナリをどのように処理すればよいですか?

ここで明確な選択肢はありません。異なるタイプのワークロード(たとえば、あるサービスでは読み取りが多い方法で使用され、別のサービスでは書き込みが多い方法で使用されるデータベース)に使用される単一のバイナリでは、異なるホットコンポーネントを持つ可能性があり、異なる最適化によってメリットが得られる可能性があります。

3つのオプションがあります。

  1. ワークロードごとに異なるバージョンのバイナリをビルドする:各ワークロードからのプロファイルを使用して、ワークロード固有の複数のバイナリビルドをビルドします。これにより、各ワークロードに最適なパフォーマンスが提供されますが、複数のバイナリとプロファイルソースを処理する点で運用上の複雑さが増す可能性があります。

  2. 「最も重要な」ワークロードのプロファイルのみを使用して単一のバイナリをビルドする:「最も重要な」ワークロード(フットプリントが最大、パフォーマンスに最も敏感)を選択し、そのワークロードからのプロファイルのみを使用してビルドします。これにより、選択したワークロードに最適なパフォーマンスが提供され、ワークロード間で共有される共通コードへの最適化により、他のワークロードでも適度なパフォーマンス向上が得られる可能性があります。

  3. ワークロード間でプロファイルをマージする:各ワークロードからのプロファイル(合計フットプリントで重み付け)を取得し、それらを単一の「フリート全体の」プロファイルにマージして、ビルドに使用される単一の共通プロファイルを作成します。これにより、すべてのワークロードで適度なパフォーマンス向上が得られる可能性があります。

PGOはビルド時間にどのように影響しますか?

PGOビルドを有効にすると、パッケージのビルド時間が大幅に増加する可能性があります。この最も顕著な要素は、PGOプロファイルがバイナリ内のすべてのパッケージに適用されるため、プロファイルの最初の使用には、依存関係グラフ内のすべてのパッケージの再ビルドが必要になることです。これらのビルドは他のビルドと同様にキャッシュされるため、同じプロファイルを使用した後続のインクリメンタルビルドでは、完全な再ビルドは必要ありません。

ビルド時間が大幅に増加する場合は、go.dev/issue/newで問題を報告してください。

注:コンパイラによるプロファイルの解析も、特に大規模なプロファイルでは大きなオーバーヘッドを追加する可能性があります。大規模なプロファイルを大規模な依存関係グラフで使用すると、ビルド時間が大幅に増加する可能性があります。これはgo.dev/issue/58102で追跡されており、将来のリリースで対処される予定です。

PGOはバイナリサイズにどのように影響しますか?

PGOは、追加の関数インライン化により、わずかに大きなバイナリになる可能性があります。

付録: プロファイル代替ソース

Goランタイムによって生成されたCPUプロファイル(runtime/pprofなどを介して)は、PGO入力として直接使用できる正しい形式です。ただし、組織は代替の推奨ツール(たとえば、Linux perf)や、Go PGOで使用したい既存のフリート全体の継続的なプロファイリングシステムを持っている可能性があります。

代替ソースからのプロファイルは、次の一般的な要件に従う限り、pprof形式に変換すれば、Go PGOで使用できます。

注:Go 1.21より前では、DWARFメタデータは関数の開始行(DW_AT_decl_line)を省略しており、ツールが開始行を特定するのが難しい場合があります。

特定のサードパーティツールのPGO互換性に関する追加情報については、Go WikiのPGO Toolsページを参照してください。