Slide 1

Slide 1 text

PGOによる コンパイラ最適化 シアラナ Go 1.20 Release Party 2023-02-21

Slide 2

Slide 2 text

自己紹介 ● シアラナ ○ Twitter: cia_rana / Zenn: koya_iwamura ● 株式会社アプリボット ○ 新規ゲームプロジェクトでサーバーサイドエンジニア ● Goの記事書いたり、コントリビュートしたり

Slide 3

Slide 3 text

目次 ● 通常のコンパイラ最適化とPGOによるコンパイラ最適化 ● PGOの使用例 & ベンチマーク ● PGO FAQ ● PGOの今後

Slide 4

Slide 4 text

通常のコンパイラ最適化 ● インライン展開 ○ 関数の呼び出し元に関数の呼び出し先を展開する ● エスケープ解析 ○ 変数の値をヒープに退避しなくて良いか判定する ● devirtualization ○ interfaceのメソッド呼び出しを具象型のメソッド呼び出しに変換する ● SSA最適化 ○ SSA: ASTからマシンコードに変換するまでの間の中間コード ○ $ go tool compile -d ssa/help で詳細が見れます

Slide 5

Slide 5 text

通常のコンパイラ最適化 ● 一見さまざまな最適化が施されているように見える... ● ただ、これはあくまでも動作させる前のコードを静的解析し、 決められたルールに従って最適化を行っているに過ぎない

Slide 6

Slide 6 text

PGOによるコンパイラ最適化 ● PGO: Profile-Guided Optimization ● ランタイム時のCPUやRAMの利用状況、関数呼び出しの頻度などを収集し、 次回のコンパイラ最適化にフィードバックする ● 実際のワークロードにより特化したコンパイラ最適化を行える ● いわば「推測するな、計測せよ」に則った最適化手法

Slide 7

Slide 7 text

Go 1.20 で導入された PGO ● 2~4%の実行速度高速化が見込めるらしい ● 今回入ったのはインライン最適化のみ ○ その他の最適化については最後の方で ● PGO で使用するプロファイルはpprofのプロファイル結果を用いる ○ pprofのプロファイル結果には PGOに必要な情報が含まれていなかったり、 PGOに不必要な 情報が多くファイルサイズが大きくなる傾向があり、今後別の形式が導入される可能性が あることが示唆されている(確度はまだ低い) ○ 別の形式が定義される場合は pprofのプロファイル結果から変換するツールを提供する とのこと ○ https://github.com/golang/go/issues/55022#issuecomment-1244259222 ● 今回のリリースではまだパブリックプレビュー版なので、本番環境での利用は避け る

Slide 8

Slide 8 text

PGO 使用例 & ベンチマーク

Slide 9

Slide 9 text

実行環境 ● OS: macOS Monterey ● CPU: Intel Core i9, 8core, 2.4GHz ● RAM: 32GB ● Go: 1.20.0

Slide 10

Slide 10 text

使用するコード ● GopherCon 2019で行われた「High Performance Go Workshop」の mandelwebを用いて、ベンチマークを取ってみる ● mandelwebはマンデルブロ集合を生成するWebサーバー https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html

Slide 11

Slide 11 text

1. プロファイルを取る手段を選択する プロファイルの取り方は大きく分けて3つ ● github.com/pkg/profile パッケージを用いる ● net/http/pprof パッケージを用いる ● ベンチマークで取得する

Slide 12

Slide 12 text

1. プロファイルを取る手段を選択する プロファイルの取り方は大きく分けて3つ ● github.com/pkg/profile パッケージを用いる ● net/http/pprof パッケージを用いる ● ベンチマークで取得する

Slide 13

Slide 13 text

2. PGOを無効にしてビルドする 現在のディレクトリ構成 $ tree ├── cmd │ └── mandelweb │ ├── benchmark_test.go │ └── main.go └── go.mod

Slide 14

Slide 14 text

2. PGOを無効にしてビルドする $ go build -pgo=off -o mandelweb_nopgo ./cmd/mandelweb/ ● Go 1.20から追加されたビルドフラグ ● offに設定することでプロファイルを明示的に使わない設定にできる

Slide 15

Slide 15 text

2. PGOを無効にしてビルドする $ ./mandelweb_nopgo ● PGOを無効にしてビルドした Webサーバ ● ポート8080で待機中 ● エンドポイント /mandelbrot でマンデルブロ集合を描画して返す

Slide 16

Slide 16 text

3. ベンチマークを取る(PGO無効) Webサーバーが起動しているターミナルとは別ターミナルでベンチマークを取る $ go test -bench=. -count=30 ./cmd/mandelweb/benchmark_test.go > benchmark_nopgo.txt ● PGOを無効にしてビルドした Webサーバーに対してリクエストを 送るベンチマーク func Benchmark(b *testing.B) { for i := 0; i < b.N; i++ { resp, err := http.Get("http://127.0.0.1:8080/mandelbrot") if err != nil { b.Fatal(err.Error()) } resp.Body.Close() } }

Slide 17

Slide 17 text

4. プロファイルを取る Apache Bench で負荷をかける $ ab -n 1000 -c 10 http://127.0.0.1:8080/mandelbrot ● -n: 最大リクエスト数 ● -c: 同時リクエスト数 ● つまり10並列で合計1000回リクエストする

Slide 18

Slide 18 text

4. プロファイルを取る 負荷がかかっている間にプロファイルを取る $ curl -o cmd/mandelweb/default.pgo http://127.0.0.1:8080/debug/pprof/profile?seconds=10 ● 保存するファイル名 ● main パッケージと同じディレ クトリに保存する ● 10秒間プロファイルを取る ● 内部で net/http/pprof が 使用されている

Slide 19

Slide 19 text

4. プロファイルを取る 現在のディレクトリ構成 $ tree ├── benchmark_nopgo.txt ├── cmd │ └── mandelweb │ ├── benchmark_test.go │ ├── default.pgo │ └── main.go ├── go.mod └── mandelweb_nopgo

Slide 20

Slide 20 text

5. PGOを有効にしてビルドする $ go build -pgo=auto -o mandelweb_pgo ./cmd/mandelweb/ ● 無効時はoffを設定していたが、有効時は autoを設定する ● autoにするとmainパッケージのdefault.pgoを探索して使用する ● -pgo=./cmd/mandelweb/default.pgo のようにファイルパスを 指定することもできる ● Go 1.20ではデフォルトでoffだが、将来的にデフォルトで autoに なることが示唆されている

Slide 21

Slide 21 text

5. PGOを有効にしてビルドする $ ./mandelweb_pgo ● PGOを有効にしてビルドした Webサーバを起動 ● 挙動はPGOを無効にしてビルドした mandelweb_nopgoと同じ

Slide 22

Slide 22 text

6. ベンチマークを取る(PGO有効) Webサーバーを起動しているターミナルとは別ターミナルでベンチマークを取る $ go test -bench=. -count=30 ./cmd/mandelweb/benchmark_test.go > benchmark_pgo.txt ● PGOを有効にしてビルドした Webサーバーに対してリクエストを 送るベンチマーク ● ベンチマーカーの中身自体は PGOを 無効にしていたときと同じ

Slide 23

Slide 23 text

7. ベンチマーク結果を比較する PGOを無効・有効にしたWebサーバーに対しそれぞれベンチマークを 行なった結果を benchstat で比較する $ benchstat benchmark_nopgo.txt benchmark_pgo.txt ● go install golang.org/x/perf/cmd/benchstat@latest でインストール

Slide 24

Slide 24 text

7. ベンチマーク結果を比較する PGOを有効にすると逆に実行時間が増えてしまった😇 benchmark_nopgo.txt benchmark_pgo.txt vs 41.62 msec/op ± 2% 42.63 msec/op ± 1% +2.41% (p=0.000 n=30)

Slide 25

Slide 25 text

7. ベンチマーク結果を比較する コンパイラ最適化結果を比較してみる $ go build -pgo=off -o mandelbrot_nopgo -gcflags="-m" ./cmd/mandelweb/ 2> opt_nopgo.txt $ go build -pgo=auto -o mandelbrot_pgo -gcflags="-m" ./cmd/mandelweb/ 2> opt_pgo.txt ● ビルド時に-gcflagsに-mをつけると、 最適化結果を出力してくれる

Slide 26

Slide 26 text

7. ベンチマーク結果を比較する コンパイラ最適化結果を比較してみる $ diff opt_nopgo.txt opt_pgo.txt > cmd/mandelweb/main.go:59:15: inlining call to fillPixel func fillPixel(m *Img, x, y int) { const n = 1000 const Limit = 2.0 const Zoom = 4 Zr, Zi, Tr, Ti := 0.0, 0.0, 0.0, 0.0 Cr := Zoom*float64(x)/float64(n) - 1.5 Ci := Zoom*float64(y)/float64(n) - 1.0 for i := 0; i < n && (Tr+Ti <= Limit*Limit); i++ { Zi = 2*Zr*Zi + Ci Zr = Tr - Ti + Cr Tr = Zr * Zr Ti = Zi * Zi } paint(&m.m[x][y], Tr, Ti) }

Slide 27

Slide 27 text

8. ベンチマーク結果の考察 なぜパフォーマンスが落ちたのか?(予想) ● pprofでプロファイルを取得した時のワークロードとベンチマークコードが 違うから ● インライン展開は特に処理サイズ小さく頻繁に呼び出される関数に対しては有効に 働くが、fillPixelはサイズが大きく、CPUの命令キャッシュや レジスタを圧迫している可能性があるから ● mandelwebの他にも3つほどベンチマークケースを作ったが、 どれもパフォーマンスが改善しなかったので、実行環境由来の可能性もある ● パブリックプレビュー版なので、まだ完璧に機能するわけではない

Slide 28

Slide 28 text

PGO FAQ

Slide 29

Slide 29 text

PGOによる最適化が行われるのはユーザーコードだけか? ● ユーザーコードはもちろん、標準パッケージや3rd partyのパッケージに 対してもコンパイラ最適化は行われる

Slide 30

Slide 30 text

異なる GOARCH/GOOS のプロファイル結果を利用できるか? ● プロファイルの形式はCPUアーキテクチャなどによらないため、 基本的には利用できる ● そのため、プロファイル結果をmainパッケージのディレクトリに配置して リポジトリにコミットすることが推奨されている ● ただし、ビルドタグによってビルドされるファイルが異なるとホットな部分が見逃され る場合がある

Slide 31

Slide 31 text

単一バイナリで複数ワークロードを扱う場合は? 次の3つの解決方法が提示されているがトレードオフがあるので、 メリデメを考えて採用する 1. バイナリを単一ではなくワークロードごとに分ける ○ それぞれのワークロードに対し最高のフォーマンス改善を期待できるが、 複数バイナリを管理する煩雑さはある 2. パフォーマンスを最も重視するワークロードに対してPGOを適用する ○ そのワークロードについては最高のパフォーマンス改善を期待できるが、 その他のワークロードのパフォーマンスはそこそこの改善に止まる 3. 複数ワークロードのプロファイル結果をマージする ○ 2つの目の解決方法のその他のワークロードに比べると、 パフォーマンスの改善を期待できるが、最高のパフォーマンスは期待できない ○ プロファイルのマージは $ go tool pprof -proto a.pprof b.pprof > c.pprof で行える

Slide 32

Slide 32 text

ビルド時間は長くならないか? ● PGOを有効にしない場合に比べて伸びる ● 2回目以降のビルドで同じプロファイルを使用する場合は、 ビルドキャッシュが作られるためビルド時間は抑えられる ● プロファイルが大きいと解析するのに時間がかかる問題がある ○ https://github.com/golang/go/issues/58102

Slide 33

Slide 33 text

PGOはどのように運用するの? ● Go PGOはAutoFDOというフローに基づくように設計されている 1. PGOを無効にしてビルドしたバイナリをプロダクションにリリース 2. プロダクションからプロファイルを収集 3. 次回リリース時にPGOを有効にして収集したプロファイルを用いてビルド &リリース 4. 2. 3. を繰り返す ● AutoFDOは単純だが、注意すべき点とそれを防ぐ考えがある ○ ソース安定性(Source stability) ○ 反復安定性(Iterative stability)

Slide 34

Slide 34 text

ソース安定性 ● 一般的にリリース後にはソースは改変されるため、プロファイルを取ったソースと次にビル ドするときのソースは違う ● ソースが改変されてもプロファイルをなるべく活用できるように最善を尽くすことが ソース安 定性 ● ソース安定性が崩れない条件 ○ ホットな関数外への改変 ○ 同じパッケージの違うファイルへホットな関数を移動 ● ソース安定性が崩れる条件 ○ ホットな関数の改変 ○ 関数名の改変 ○ 違うパッケージへホットな関数を移動

Slide 35

Slide 35 text

反復安定性 ● PGOによってホットとみなされ最適化された関数は、次のビルドではホットではなく なっているはずなので最適化が行われない ● そして最適化が行われていない関数が、その次のビルド時に再度ホットをみなされ る ● つまり、ある関数がPGOを用いてビルドを行うたびに、最適化が行われた状態と行 われていない状態を繰り返す可能性がある ● Goはこの反復を行わないように保守的にPGOを活用している ● このことを反復安定性と呼ぶ

Slide 36

Slide 36 text

今後予定されている最適化項目 ● slice/mapのcapサイズを事前に決める ● devirtualization ● 実行バイナリ内での関数の配置順序を変える ● 寿命の近い値を近い場所にメモリ割り当てする ● レジスタ割り当ての効率化 ● など https://github.com/golang/go/issues/55022#issuecomment-1245605666

Slide 37

Slide 37 text

もっと詳しく知りたい人向け ● Proposal: profile-guided optimization ○ ベースとなるProposal ○ https://go.googlesource.com/proposal/+/master/design/55022-pgo.md ● Proposal: Design and Implementation of Profile-Guided Optimization (PGO) for Go ○ 実装方針を説明した Proposal ○ https://go.googlesource.com/proposal/+/master/design/55022-pgo-implementation.md ● PGOの初期実装 ○ https://go-review.googlesource.com/c/go/+/429863 ● PGOの課題の中でGo 1.21で解決予定のリスト ○ https://github.com/golang/go/issues/55022#issuecomment-1409183967