はじめに
Go 1.21 から Profile-Guided Optimization (PGO) が正式に GA となり、default.pgo をメインパッケージに配置するか、-pgo フラグで PGO プロファイルを指定することで PGO を適用できるようになりました。しかし、Bazel を使用してビルドしている環境では、これらの仕組みが機能しないため、rules_go を使った別の設定が必要になります。
本記事では、Bazel (rules_go) を利用して Go の PGO ビルドを行う方法について解説します。
前提
本記事では以下のバージョンを前提としています。
- Go: 1.25
- Bazel: 8.2.1
- Bazelisk: 1.28.1
- rules_go: 0.60.0
また、本記事中で使用するサンプルコードは、以下のリポジトリで公開しています。
PGO (Profile-Guided Optimization) とは
PGO は、プログラムの実行時プロファイル情報をコンパイラにフィードバックし、その情報に基づいてコンパイル時の最適化を行う手法です。一般的なコンパイラの最適化はソースコードの静的解析に基づいて行われますが、PGO では実際の実行パターンを考慮するため、より効果的な最適化が可能になります。
Go では 1.20 でプレビューとして導入され、1.21 で正式に GA となりました。Go の公式ドキュメントによると、PGO によって約 2-7% のパフォーマンス改善が報告されています。改善率はアプリケーションの特性に依存するため、実際の効果はワークロードによって異なります。
PGO が行う最適化
Go の PGO では、主に以下の最適化が行われます。
Inlining(インライン展開)
プロファイルでホットと判定された関数を、通常よりも積極的にインライン展開します。関数呼び出しのオーバーヘッドが削減されるだけでなく、インライン化によって呼び出し元でさらなる最適化(定数伝播、エスケープ解析の改善など)が可能になります。
Devirtualization(脱仮想化)
インタフェース値に対する間接呼び出しについて、プロファイル情報から支配的な具象型を特定し、直接呼び出しに変換します。
具体的には、以下のような変換が行われます。
// PGO 適用前(間接呼び出し)
var r io.Reader = f
r.Read(b)
// PGO 適用後(ランタイムガード付き直接呼び出し)
if f, ok := r.(*os.File); ok {
f.Read(b) // 直接呼び出し → さらにインライン化が可能
} else {
r.Read(b) // フォールバック
}
プロファイル情報から r の具象型が高い確率で *os.File であると判定された場合、型アサーションによるガード付きの直接呼び出しに変換します。直接呼び出しはさらにインライン展開の対象となり、連鎖的な最適化が期待できます。
プロファイルの取得
PGO ビルドを行うためには、まず対象アプリケーションの CPU プロファイルを取得する必要があります。Go では net/http/pprof パッケージを使用することで、HTTP エンドポイント経由でプロファイルを取得することができます。
以下は、pprof エンドポイントを有効にしたサーバーの例です。
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof" // /debug/pprof/ エンドポイントを登録
)
func main() {
http.HandleFunc("/health", handleHealth)
// ... 省略 ...
log.Println("server listening on :8080")
log.Println("pprof available at /debug/pprof/")
log.Fatal(http.ListenAndServe(":8080", nil))
}
_ "net/http/pprof" をインポートすることで、/debug/pprof/ 以下に各種プロファイリング用のエンドポイントが自動的に登録されます。CPU プロファイルは以下のエンドポイントから取得できます。
$ curl -o profile/default.pgo "http://localhost:8080/debug/pprof/profile?seconds=30"
すでに Grafana Pyroscope や Datadog などでプロファイルの収集を行っている場合は、それぞれのプロダクトが提供している CLI を使用して PGO プロファイルをエクスポートすることも可能です。本番環境で利用を検討する際は、利用している Observability ツール側でサポートされているかを確認することをお勧めします。
- Pyroscope: grafana/pyroscope/cmd/profilecli
- Datadog: DataDog/datadog-pgo
Bazel で PGO ビルドを行う
Bazel ターゲットとしてプロファイルを定義する
PGO プロファイルを Bazel のビルドターゲットとして使用するために、profile/BUILD.bazel で exports_files を定義します。
exports_files(
["default.pgo"],
visibility = ["//visibility:public"],
)
PGO の指定方法
rules_go では、PGO プロファイルを指定する方法が 2 つ用意されています。
1. go_binary の pgoprofile 属性
go_binary ルールの pgoprofile 属性にプロファイルファイルのラベルを指定します。
load("@rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "server_lib",
srcs = ["main.go"],
importpath = "github.com/ucpr/workspace2026/bazel_go_pgo_build/cmd/server",
visibility = ["//visibility:private"],
)
go_binary(
name = "server",
embed = [":server_lib"],
pgoprofile = "//profile:default.pgo",
visibility = ["//visibility:public"],
)
この方法では、pgoprofile 属性に指定されたプロファイルが、バイナリに依存するすべての go_library のコンパイルにも適用されます。内部的には Bazel の configuration transition を使って実装されているため、プロファイルの指定が依存グラフ全体に伝播します。
2. コマンドラインフラグ
ビルド時にコマンドラインフラグで PGO プロファイルを指定することもできます。
$ bazel build //cmd/server \
--@rules_go//go/config:pgoprofile=//profile:default.pgo
この方法は .bazelrc に設定を記述することで、恒久的な設定としても使用することができます。
build --@rules_go//go/config:pgoprofile=//profile:default.pgo
プロファイルが存在しない場合の挙動
pgoprofile 属性やコマンドラインフラグで指定した PGO プロファイルが存在しない場合、ビルドは missing input file エラーで失敗します。
$ bazel build //cmd/server
ERROR: missing input file '//profile:default.pgo'
ERROR: Build did NOT complete successfully
PGO を一時的に無効化したい場合は、pgoprofile 属性をコメントアウトするか、コマンドラインフラグでの指定を削除する必要があります。
PGO ビルドされたかの確認方法
通常の go build で PGO を適用した場合、debug.ReadBuildInfo() や go version -m で -pgo フラグの有無を確認することができます。
$ go version -m /path/to/binary
# ...
# build -pgo=/path/to/default.pgo
プログラム内から確認する場合は、以下のように debug.ReadBuildInfo() の Settings から -pgo キーを検索します。
func isPGOEnabled() bool {
info, ok := debug.ReadBuildInfo()
if ok {
for _, bs := range info.Settings {
if bs.Key == "-pgo" {
return bs.Value != ""
}
}
}
return false
}
しかし、Bazel でビルドした場合は debug.ReadBuildInfo() がビルド情報を返さない(false を返す)ため、上記のいずれの方法でも確認することができません。
Bazel でビルドしたバイナリに PGO が適用されているかを確認するには、--subcommands フラグを使用してビルドログを確認します。
$ bazel build //cmd/server --subcommands 2>&1 | grep pgoprofile
--subcommands はキャッシュヒット時にはビルドコマンドが出力されません。確認する際は bazel clean を実行してからビルドしてください。ビルドログに -pgoprofile profile/default.pgo が含まれていれば、PGO が適用されていることが確認できます。
# stdlib のビルドに PGO が適用されている
bazel-out/.../builder stdlib ... -pgoprofile profile/default.pgo)
# アプリケーションコードのコンパイルに PGO が適用されている
bazel-out/.../builder compilepkg ... -pgoprofile profile/default.pgo)
おわりに
本記事では、Go の PGO の概要と、Bazel (rules_go) を使用して PGO ビルドを行う方法について解説しました。
rules_go では go_binary の pgoprofile 属性とコマンドラインフラグの 2 つの方法で PGO を利用することができます。Go のバージョンが上がるにつれて PGO の最適化範囲も拡大しているため、パフォーマンスが重要なアプリケーションでは導入を検討する価値があると考えられます。
本記事の内容に誤りや不正確な表現がありましたら、ご連絡いただけると幸いです。