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 https://127.0.0.1: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,它會自動為伺服器新增一個 /debug/pprof/profile 端點,用於擷取 CPU 分析檔。

通常您會希望從您的生產環境中收集分析檔,如此一來編譯器便會取得生產環境中行為的代表性檢視。由於此範例沒有「生產」環境,我們會建立一個簡單的程式,以便在收集分析檔時產生負載。複製 此程式 的原始碼至 load/main.go,並啟動負載產生器(請確定伺服器仍在執行中!)。

$ go run example.com/markdown/load

當程式執行時,請從伺服器下載分析檔

$ curl -o cpu.pprof "https://127.0.0.1:8080/debug/pprof/profile?seconds=30"

完成之後,請終止負載產生器和伺服器。

使用分析檔

使用 -pgo 旗標傳遞給 go build,我們可以要求 Go 工具鏈使用 PGO 建立。-pgo 會採用要使用的分析檔路徑,或 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

評估

我們將使用負載產生器的 Go 效能評量版本來評估 PGO 對效能的影響。將 這項效能評量 複製至 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 已發布!
部落格索引