
## はじめに

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

また、本記事中で使用するサンプルコードは、以下のリポジトリで公開しています。

[workspace2026/bazel_go_pgo_build](https://github.com/ucpr/workspace2026/tree/main/bazel_go_pgo_build)

## PGO (Profile-Guided Optimization) とは

PGO は、プログラムの実行時プロファイル情報をコンパイラにフィードバックし、その情報に基づいてコンパイル時の最適化を行う手法です。一般的なコンパイラの最適化はソースコードの静的解析に基づいて行われますが、PGO では実際の実行パターンを考慮するため、より効果的な最適化が可能になります。

[Profile-guided optimization in Go 1.21 - The Go Blog](https://go.dev/blog/pgo)

Go では 1.20 でプレビューとして導入され、1.21 で正式に GA となりました。Go の公式ドキュメントによると、[PGO によって約 2-7% のパフォーマンス改善](https://go.dev/blog/pgo#:~:text=The%20new%20version%20is%20around%203.8%25%20faster!%20In%20Go%201.21%2C%20workloads%20typically%20get%20between%202%25%20and%207%25%20CPU%20usage%20improvements%20from%20enabling%20PGO)が報告されています。改善率はアプリケーションの特性に依存するため、実際の効果はワークロードによって異なります。

[実運用で考える Profile-guided optimization (PGO)](https://zenn.dev/knowledgework/articles/0574f01f54306e)

### PGO が行う最適化

Go の PGO では、主に以下の最適化が行われます。

**Inlining（インライン展開）**

プロファイルでホットと判定された関数を、通常よりも積極的にインライン展開します。関数呼び出しのオーバーヘッドが削減されるだけでなく、インライン化によって呼び出し元でさらなる最適化（定数伝播、エスケープ解析の改善など）が可能になります。

**Devirtualization（脱仮想化）**

インタフェース値に対する間接呼び出しについて、プロファイル情報から支配的な具象型を特定し、直接呼び出しに変換します。

具体的には、以下のような変換が行われます。

```go
// 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 エンドポイントを有効にしたサーバーの例です。

```go:cmd/server/main.go
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 プロファイルは以下のエンドポイントから取得できます。

```bash
$ curl -o profile/default.pgo "http://localhost:8080/debug/pprof/profile?seconds=30"
```

> [!NOTE]
> 本番環境でプロファイルを取得する場合は、代表的なワークロードが実行されている状態で取得することが重要です。偏ったプロファイルでは最適化の効果が十分に得られない可能性があります。

すでに Grafana Pyroscope や Datadog などでプロファイルの収集を行っている場合は、それぞれのプロダクトが提供している CLI を使用して PGO プロファイルをエクスポートすることも可能です。本番環境で利用を検討する際は、利用している Observability ツール側でサポートされているかを確認することをお勧めします。

- Pyroscope: [grafana/pyroscope/cmd/profilecli](https://github.com/grafana/pyroscope/tree/main/cmd/profilecli)
- Datadog: [DataDog/datadog-pgo](https://github.com/DataDog/datadog-pgo)

## Bazel で PGO ビルドを行う

### Bazel ターゲットとしてプロファイルを定義する

PGO プロファイルを Bazel のビルドターゲットとして使用するために、`profile/BUILD.bazel` で `exports_files` を定義します。

```python:profile/BUILD.bazel
exports_files(
    ["default.pgo"],
    visibility = ["//visibility:public"],
)
```

### PGO の指定方法

rules_go では、PGO プロファイルを指定する方法が 2 つ用意されています。

#### 1. `go_binary` の `pgoprofile` 属性

`go_binary` ルールの `pgoprofile` 属性にプロファイルファイルのラベルを指定します。

```python:cmd/server/BUILD.bazel
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 プロファイルを指定することもできます。

```bash
$ bazel build //cmd/server \
    --@rules_go//go/config:pgoprofile=//profile:default.pgo
```

この方法は `.bazelrc` に設定を記述することで、恒久的な設定としても使用することができます。

```bash:.bazelrc
build --@rules_go//go/config:pgoprofile=//profile:default.pgo
```

### プロファイルが存在しない場合の挙動

`pgoprofile` 属性やコマンドラインフラグで指定した PGO プロファイルが存在しない場合、ビルドは `missing input file` エラーで失敗します。

```bash
$ 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` フラグの有無を確認することができます。

```bash
$ go version -m /path/to/binary
# ...
# build	-pgo=/path/to/default.pgo
```

プログラム内から確認する場合は、以下のように `debug.ReadBuildInfo()` の Settings から `-pgo` キーを検索します。

```go
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` フラグを使用してビルドログを確認します。

```bash
$ bazel build //cmd/server --subcommands 2>&1 | grep pgoprofile
```

> [!NOTE]
> `--subcommands` はキャッシュヒット時にはビルドコマンドが出力されません。確認する際は `bazel clean` を実行してからビルドしてください。

ビルドログに `-pgoprofile profile/default.pgo` が含まれていれば、PGO が適用されていることが確認できます。

```bash
# 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 の最適化範囲も拡大しているため、パフォーマンスが重要なアプリケーションでは導入を検討する価値があると考えられます。

本記事の内容に誤りや不正確な表現がありましたら、ご連絡いただけると幸いです。

## 参考

- [Profile-guided optimization - The Go Programming Language](https://go.dev/doc/pgo)
- [Profile-guided optimization in Go 1.21 - The Go Blog](https://go.dev/blog/pgo)
- [rules_go - go_binary rule](https://github.com/bazel-contrib/rules_go/blob/master/docs/go/core/rules.md)
- [rules_go - PGO support PR #3641](https://github.com/bazel-contrib/rules_go/pull/3641)
- [How to use PGO and Grafana Pyroscope to optimize Go applications](https://grafana.com/docs/pyroscope/latest/view-and-analyze-profile-data/profile-cli/)
