The Go Blog
プロファイルに基づく最適化プレビュー
Goバイナリをビルドする際、Goコンパイラは可能な限り最高のパフォーマンスを発揮するバイナリを生成するために最適化を行います。例えば、定数伝播はコンパイル時に定数式を評価し、実行時の評価コストを回避します。エスケープ解析は、ローカルスコープのオブジェクトのヒープ割り当てを回避し、GCオーバーヘッドを回避します。インライン化は、単純な関数の本体を呼び出し元にコピーし、呼び出し元でのさらなる最適化(追加の定数伝播やより良いエスケープ解析など)を可能にすることがよくあります。
Goはリリースごとに最適化を改善していますが、これは常に簡単な作業ではありません。一部の最適化は調整可能ですが、コンパイラはすべての関数に対して「最大値に設定する」ことはできません。なぜなら、過度に積極的な最適化は、実際にはパフォーマンスを低下させたり、ビルド時間を過剰に長くしたりする可能性があるからです。他の最適化では、コンパイラが関数内の「共通」パスと「非共通」パスが何であるかについて判断を下す必要があります。コンパイラは、実行時にどのケースが共通になるかを知ることができないため、静的なヒューリスティックに基づいて最善の推測を行う必要があります。
しかし、それは可能でしょうか?
コードが本番環境でどのように使用されているかについて決定的な情報がない場合、コンパイラはパッケージのソースコードのみを操作できます。しかし、本番環境の動作を評価するためのツールはあります。プロファイリングです。コンパイラにプロファイルを提供すれば、より多くの情報に基づいた意思決定ができます。最も頻繁に使用される関数をより積極的に最適化したり、共通のケースをより正確に選択したりできます。
コンパイラ最適化のためにアプリケーション動作のプロファイルを使用することは、プロファイルに基づく最適化 (PGO) (フィードバック駆動型最適化 (FDO) とも呼ばれます) として知られています。
Go 1.20には、PGOの初期サポートがプレビューとして含まれています。完全なドキュメントについては、プロファイルに基づく最適化ユーザーガイドを参照してください。まだ本番環境での使用を妨げる可能性のある未熟な部分もありますが、ぜひ試してみて、遭遇したフィードバックや問題をお寄せください。
例
MarkdownをHTMLに変換するサービスを構築してみましょう。ユーザーはMarkdownソースを/renderにアップロードし、HTML変換が返されます。これにはgitlab.com/golang-commonmark/markdownを簡単に使用できます。
セットアップ
$ go mod init example.com/markdown
$ go get gitlab.com/golang-commonmark/markdown@bf3e522c626a
main.go内
package main
import (
"bytes"
"io"
"log"
"net/http"
_ "net/http/pprof"
"gitlab.com/golang-commonmark/markdown"
)
func render(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
return
}
src, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("error reading body: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
md := markdown.New(
markdown.XHTMLOutput(true),
markdown.Typographer(true),
markdown.Linkify(true),
markdown.Tables(true),
)
var buf bytes.Buffer
if err := md.Render(&buf, src); err != nil {
log.Printf("error converting markdown: %v", err)
http.Error(w, "Malformed markdown", http.StatusBadRequest)
return
}
if _, err := io.Copy(w, &buf); err != nil {
log.Printf("error writing response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
func main() {
http.HandleFunc("/render", render)
log.Printf("Serving on port 8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
サーバーのビルドと実行
$ go build -o markdown.nopgo.exe
$ ./markdown.nopgo.exe
2023/01/19 14:26:24 Serving on port 8080...
別のターミナルからMarkdownを送信してみましょう。GoプロジェクトのREADMEをサンプルドキュメントとして使用できます。
$ curl -o README.md -L "https://raw.githubusercontent.com/golang/go/c16c2c49e2fa98ae551fc6335215fadd62d33542/README.md"
$ curl --data-binary @README.md https://:8080/render
<h1>The Go Programming Language</h1>
<p>Go is an open source programming language that makes it easy to build simple,
reliable, and efficient software.</p>
...
プロファイリング
動作するサービスができたので、プロファイルを取得し、PGOで再構築してパフォーマンスが向上するかどうかを確認してみましょう。
main.goでは、net/http/pprofをインポートしました。これにより、CPUプロファイルを取得するための/debug/pprof/profileエンドポイントが自動的にサーバーに追加されます。
通常、コンパイラが本番環境での動作の代表的なビューを取得できるように、本番環境からプロファイルを収集したいと考えるでしょう。この例には「本番」環境がないため、プロファイルを収集しながら負荷を生成する簡単なプログラムを作成します。このプログラムのソースをload/main.goにコピーし、ロードジェネレーターを起動します(サーバーがまだ実行中であることを確認してください!)。
$ go run example.com/markdown/load
それが実行されている間に、サーバーからプロファイルをダウンロードします。
$ curl -o cpu.pprof "https://:8080/debug/pprof/profile?seconds=30"
これが完了したら、ロードジェネレーターとサーバーを停止します。
プロファイルの使用
go buildに-pgoフラグを使用して、GoツールチェインにPGOでビルドするように要求できます。-pgoは、使用するプロファイルへのパス、またはautoを受け取ります。autoは、メインパッケージディレクトリのdefault.pgoファイルを使用します。
default.pgoプロファイルをリポジトリにコミットすることをお勧めします。プロファイルをソースコードと一緒に保存することで、ユーザーはリポジトリの取得(バージョン管理システムまたはgo get経由)だけでプロファイルに自動的にアクセスでき、ビルドの再現性が保証されます。Go 1.20では、-pgo=offがデフォルトですが、ユーザーはまだ-pgo=autoを追加する必要があります。しかし、将来のバージョンのGoでは、デフォルトが-pgo=autoに変更され、バイナリをビルドする誰もが自動的にPGOの恩恵を受けられるようになる予定です。
ビルドしましょう
$ mv cpu.pprof default.pgo
$ go build -pgo=auto -o markdown.withpgo.exe
評価
PGOがパフォーマンスに与える影響を評価するために、ロードジェネレーターのGoベンチマークバージョンを使用します。このベンチマークをload/bench_test.goにコピーします。
まず、PGOなしでサーバーをベンチマークします。サーバーを起動します。
$ ./markdown.nopgo.exe
それが実行されている間に、いくつかのベンチマークイテレーションを実行します。
$ go test example.com/markdown/load -bench=. -count=20 -source ../README.md > nopgo.txt
それが完了したら、元のサーバーを停止し、PGO付きバージョンを起動します。
$ ./markdown.withpgo.exe
それが実行されている間に、いくつかのベンチマークイテレーションを実行します。
$ go test example.com/markdown/load -bench=. -count=20 -source ../README.md > withpgo.txt
それが完了したら、結果を比較してみましょう。
$ go install golang.org/x/perf/cmd/benchstat@latest
$ benchstat nopgo.txt withpgo.txt
goos: linux
goarch: amd64
pkg: example.com/markdown/load
cpu: Intel(R) Xeon(R) W-2135 CPU @ 3.70GHz
│ nopgo.txt │ withpgo.txt │
│ sec/op │ sec/op vs base │
Load-12 393.8µ ± 1% 383.6µ ± 1% -2.59% (p=0.000 n=20)
新しいバージョンは約2.6%高速です!Go 1.20では、PGOを有効にすることで、ワークロードは通常2%から4%のCPU使用率の改善を得られます。プロファイルにはアプリケーション動作に関する豊富な情報が含まれており、Go 1.20はインライン化にこの情報を使用し始めたばかりです。将来のリリースでは、コンパイラのより多くの部分がPGOを利用するにつれて、パフォーマンスは改善され続けるでしょう。
次のステップ
この例では、プロファイルを収集した後、元のビルドで使用したのとまったく同じソースコードを使用してサーバーを再構築しました。実際のシナリオでは、常に開発が進行しています。そのため、先週のコードを実行している本番環境からプロファイルを収集し、今日のソースコードでビルドするために使用することがあります。それはまったく問題ありません!GoのPGOは、ソースコードの軽微な変更を問題なく処理できます。
PGOの使用、ベストプラクティス、注意すべき点に関する詳細については、プロファイルに基づく最適化ユーザーガイドを参照してください。
皆様からのフィードバックをお待ちしております!PGOはまだプレビュー段階であり、使いにくい点、正しく動作しない点など、どんなことでもお聞かせいただければ幸いです。go.dev/issue/newに問題を報告してください。
謝辞
Goへのプロファイルに基づく最適化の追加はチームの努力であり、特にUberのRaj BarikとJin Lin、GoogleのCherry MuiとAustin Clementsの貢献に感謝したいと思います。この種のコミュニティ間のコラボレーションは、Goを素晴らしいものにする上で重要な部分です。
次の記事:すべての比較可能な型
前の記事:Go 1.20がリリースされました!
ブログインデックス