Go言語入門:プロファイルガイド最適化 -Vol.5-

スポンサーリンク
Go言語入門:プロファイルガイド最適化 -Vol.5- ノウハウ
Go言語入門:プロファイルガイド最適化 -Vol.5-
この記事は約10分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語のプロファイルガイド最適化ついて解説しています。

スポンサーリンク

背景

Go言語でアプリケーションのパフォーマンスを改善したいと思ったことはありませんか? 公式ドキュメントで「Profile-guided optimization (PGO)」という機能を見かけたものの、英語で書かれていて専門用語も多く、「どういう仕組みなんだろう?」「自分のプロジェクトにどう活用すればいいの?」と戸惑った経験があるかもしれません。

この記事では、Go 1.21から正式サポートされたPGO(プロファイルガイド最適化)について、公式ドキュメントの丁寧な日本語訳と、初心者の方にもわかりやすい補足説明をお届けします。PGOは実行時のプロファイル情報をコンパイラにフィードバックすることで、2〜14%のパフォーマンス向上が期待できる強力な機能です。

「コンパイラ最適化」や「プロファイリング」と聞くと難しそうに感じるかもしれませんが、基本的な使い方はとてもシンプルです。この記事では、PGOの仕組みを身近な例えで説明し、実際にどうやって使うのかを具体的なコード例とともに紹介していきます。一緒に学んでいきましょう!

プロファイルのマージ

pprofツールは、次のように複数のプロファイルをマージできます:

$ go tool pprof -proto a.pprof b.pprof > merged.pprof

このマージは、プロファイルの実時間(wall duration)に関係なく、入力内のサンプルを単純に合計する形で行われます。その結果、アプリケーションの小さな時間スライスをプロファイリングする場合(例:無期限に実行されるサーバー)、すべてのプロファイルが同じ実時間を持つようにする必要があります(つまり、すべてのプロファイルを30秒間収集する)。そうしないと、より長い実時間のプロファイルがマージされたプロファイルで過剰に表現されてしまいます。


解説

プロファイルのマージ:単純な足し算

pprofのマージは非常にシンプルで、各プロファイルのサンプルを単純に合計します。

イメージ:投票の集計

プロファイルA(30秒間):
関数X: 100回呼ばれた
関数Y: 50回呼ばれた

プロファイルB(30秒間):
関数X: 80回呼ばれた
関数Y: 60回呼ばれた

マージ結果:
関数X: 100 + 80 = 180回
関数Y: 50 + 60 = 110回

これは直感的でわかりやすいですね。

問題:収集時間が異なる場合の罠

しかし、収集時間が異なるプロファイルを混ぜると偏りが生じます

悪い例:収集時間が異なる

プロファイルA(30秒間):
関数X: 100回
関数Y: 50回

プロファイルB(60秒間):  ← 2倍の時間
関数X: 160回
関数Y: 100回

マージ結果:
関数X: 100 + 160 = 260回
関数Y: 50 + 100 = 150回

問題点: プロファイルBは2倍の時間をかけて収集されているため、2倍の重みを持ってしまいます。

具体例で理解する:レストランの来客数調査

レストランの例えで考えてみましょう。

公平な調査(収集時間が同じ):

店舗A: 1時間調査 → パスタ注文30件、ピザ注文20件
店舗B: 1時間調査 → パスタ注文25件、ピザ注文25件

合計:
パスタ: 30 + 25 = 55件
ピザ: 20 + 25 = 45件

結論: パスタの方が人気(55 vs 45)

不公平な調査(収集時間が異なる):

店舗A: 1時間調査 → パスタ30件、ピザ20件
店舗B: 3時間調査 → パスタ75件、ピザ75件
                   (実際は1時間あたり25件、25件)

合計:
パスタ: 30 + 75 = 105件
ピザ: 20 + 75 = 95件

結論: パスタの方が人気(105 vs 95)← 正しいが、
       店舗Bの影響が3倍になっている

店舗Bの調査時間が3倍なので、店舗Bの特徴が結果を支配してしまいます。

実際のプロファイリングでの影響

シナリオ:3つのサーバーからプロファイル収集

間違った収集方法:

# サーバー1: 30秒
curl "http://server1/debug/pprof/profile?seconds=30" > server1.pprof

# サーバー2: 60秒(間違って長く取ってしまった)
curl "http://server2/debug/pprof/profile?seconds=60" > server2.pprof

# サーバー3: 30秒
curl "http://server3/debug/pprof/profile?seconds=30" > server3.pprof

# マージ
go tool pprof -proto server1.pprof server2.pprof server3.pprof > merged.pprof

結果:サーバー2の特徴が2倍の重みを持つ

もしサーバー2だけが特殊な処理をしていたら:
サーバー1: 通常処理中心(30秒分)
サーバー2: 画像処理中心(60秒分)← 2倍の影響力
サーバー3: 通常処理中心(30秒分)

マージ結果: 画像処理が過剰に最適化される

正しい収集方法:統一された時間

すべて同じ時間で収集:

# すべて30秒で統一
curl "http://server1/debug/pprof/profile?seconds=30" > server1.pprof
curl "http://server2/debug/pprof/profile?seconds=30" > server2.pprof
curl "http://server3/debug/pprof/profile?seconds=30" > server3.pprof

# マージ
go tool pprof -proto server1.pprof server2.pprof server3.pprof > merged.pprof

結果:各サーバーが公平に表現される

サーバー1: 通常処理(30秒分、重み 1)
サーバー2: 画像処理(30秒分、重み 1)
サーバー3: 通常処理(30秒分、重み 1)

マージ結果: バランスの取れた最適化

実践例:推奨される収集スクリプト

良い例:時間を統一したスクリプト

#!/bin/bash
# collect_profiles.sh

DURATION=30  # すべて30秒で統一
SERVERS=("server1" "server2" "server3")

for server in "${SERVERS[@]}"; do
    echo "Collecting from $server for ${DURATION}s..."
    curl "http://${server}:6060/debug/pprof/profile?seconds=${DURATION}" \
        > "${server}.pprof"
    
    # 念のため実際の時間を確認
    go tool pprof -raw "${server}.pprof" | grep "Duration:"
done

# すべてマージ
echo "Merging profiles..."
go tool pprof -proto "${SERVERS[@]/%/.pprof}" > merged.pprof

echo "Merged profile created: merged.pprof"

例外:意図的に重み付けしたい場合

時には、特定のプロファイルに意図的に重みを付けたい場合もあります。

ユースケース:ピーク時を重視

# ピーク時(昼): 60秒 → 2倍の重み
curl "http://server/debug/pprof/profile?seconds=60" > peak.pprof

# オフピーク時(深夜): 30秒 → 1倍の重み
curl "http://server/debug/pprof/profile?seconds=30" > offpeak.pprof

# マージ: ピーク時が2倍重視される
go tool pprof -proto peak.pprof offpeak.pprof > weighted.pprof

この場合、意図的に収集時間を変えることで、ピーク時の動作を重視した最適化ができます。

重み付けの計算例:

ピーク時の実際の負荷: 1000 req/sec
オフピークの実際の負荷: 200 req/sec

比率: 1000:200 = 5:1

重み付け収集:
ピーク時: 50秒
オフピーク時: 10秒

プロファイルの時間を確認する方法

収集したプロファイルの実時間を確認するには:

# プロファイルの詳細情報を表示
go tool pprof -raw profile.pprof | head -n 20

# 出力例:
# PeriodType: cpu nanoseconds
# Period: 10000000
# Duration: 30.01s        ← これが実時間
# Samples:
# ...

スクリプトで自動確認:

#!/bin/bash
# check_profile_duration.sh

for profile in *.pprof; do
    echo -n "$profile: "
    go tool pprof -raw "$profile" | grep "Duration:" | awk '{print $2}'
done

# 出力例:
# server1.pprof: 30.01s
# server2.pprof: 60.23s  ← 異なる時間を検出
# server3.pprof: 30.02s

トラブルシューティング:時間が異なってしまった場合

もし既に異なる時間のプロファイルを収集してしまった場合の対処法:

方法1:正規化(手動計算)

実際には、pprofツールは自動正規化をサポートしていないため、正規化は再収集が最善です。

# 悪い例:既に収集済み
# server1.pprof: 30秒
# server2.pprof: 60秒
# server3.pprof: 30秒

# 最善の対処: server2を再収集
curl "http://server2/debug/pprof/profile?seconds=30" > server2_fixed.pprof

# 修正版でマージ
go tool pprof -proto server1.pprof server2_fixed.pprof server3.pprof > merged.pprof

方法2:長い方に合わせる

# 既存:
# server1.pprof: 30秒
# server2.pprof: 60秒

# server1を60秒で再収集
curl "http://server1/debug/pprof/profile?seconds=60" > server1_60s.pprof

# 統一された時間でマージ
go tool pprof -proto server1_60s.pprof server2.pprof > merged.pprof

ベストプラクティス:収集時間の標準化

推奨される標準時間:

短時間サービス(CLI ツールなど):
→ 実行時間全体をプロファイリング

長時間サービス(Webサーバーなど):
→ 30秒(標準)
→ 60秒(より詳細が必要な場合)
→ 120秒(非常に複雑な処理の場合)

チームでの標準化:

# チーム共通の設定ファイル
# .profile_config

PROFILE_DURATION=30
PROFILE_OUTPUT_DIR=./profiles
PROFILE_SERVERS=(prod-1 prod-2 prod-3)

# すべてのスクリプトでこの設定を使用

チェックリスト:プロファイルマージの注意点

  • ✅ すべてのプロファイルを同じ時間(30秒など)で収集
  • ✅ 収集後に実時間を確認
  • ✅ 異なる時間のプロファイルは再収集
  • ✅ 意図的な重み付けは明確に文書化
  • ✅ 収集スクリプトで時間を統一
  • ❌ 異なる時間のプロファイルを無意識にマージしない
  • ❌ 「大体同じくらい」という曖昧さを許容しない
  • ❌ 確認せずにマージしない

まとめ

プロファイルのマージは単純な足し算なので、収集時間が異なると意図しない重み付けが発生します。すべてのプロファイルを同じ時間(通常30秒)で収集することが、公平でバランスの取れたPGO最適化を実現する鍵です。収集時間を統一することを忘れずに、スクリプトで自動化して人為的ミスを防ぎましょう。意図的に重み付けする場合は、その理由と計算根拠を明確に文書化しておくことが重要です。

おわりに 

本日は、Go言語のプロファイルガイド最適化について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました