Goブログ

プロファイルガイド付き最適化プレビュー

Michael Pratt
2023年2月8日

Goバイナリをビルドすると、Goコンパイラは、可能な限り最高のパフォーマンスを発揮するバイナリを生成しようと最適化を実行します。たとえば、定数伝播はコンパイル時に定数式を評価し、実行時の評価コストを回避できます。エスケープ解析は、ローカルスコープのオブジェクトのヒープ割り当てを回避し、GCオーバーヘッドを回避します。インライン化は、単純な関数の本体を呼び出し元にコピーし、多くの場合、呼び出し元でのさらなる最適化(追加の定数伝播やより良いエスケープ解析など)を可能にします。

Goはリリースごとに最適化を改善していますが、これは必ずしも簡単な作業ではありません。一部の最適化は調整可能ですが、過度に積極的な最適化はパフォーマンスを低下させたり、ビルド時間が過剰になったりする可能性があるため、コンパイラはすべての関数で単に「11に上げる」ことはできません。他の最適化では、コンパイラが関数内の「共通」および「まれな」パスを判断する必要があります。コンパイラは、実行時にどのケースが一般的になるかを知ることができないため、静的ヒューリスティックに基づいて最善の推測を行う必要があります。

またはできる?

コードが本番環境でどのように使用されるかに関する決定的な情報がないため、コンパイラはパッケージのソースコードでのみ動作できます。しかし、本番環境での動作を評価するためのツールがあります。それはプロファイリングです。プロファイルをコンパイラに提供すると、より多くの情報に基づいた決定を下すことができます。最も頻繁に使用される関数をより積極的に最適化したり、一般的なケースをより正確に選択したりできます。

コンパイラの最適化にアプリケーションの動作のプロファイルを使用することは、プロファイルガイド付き最適化(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 http://localhost: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 "http://localhost:8080/debug/pprof/profile?seconds=30"

これが完了したら、負荷ジェネレーターとサーバーを停止します。

プロファイルの使用

Goツールチェーンに、go buildへの-pgoフラグを使用してPGOでビルドするように依頼できます。-pgoは、使用するプロファイルへのパス、またはメインパッケージディレクトリのdefault.pgoファイルを使用するautoのいずれかを受け取ります。

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を有効にすることでCPU使用率が2%から4%向上します。プロファイルには、アプリケーションの動作に関する豊富な情報が含まれており、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がリリースされました!
ブログインデックス