Go 部落格

基於配置檔案的最佳化預覽

Michael Pratt
2023 年 2 月 8 日

當您構建 Go 二進位制檔案時,Go 編譯器會執行最佳化,以嘗試生成效能最佳的二進位制檔案。例如,常量傳播可以在編譯時評估常量表達式,從而避免執行時評估成本。逃逸分析可以避免為區域性作用域物件分配堆記憶體,從而避免 GC 開銷。內聯是將簡單函式的函式體複製到呼叫者中,這通常可以使呼叫者進行進一步的最佳化(例如,額外的常量傳播或更好的逃逸分析)。

Go 的最佳化在版本之間不斷改進,但這並非易事。一些最佳化是可以調整的,但編譯器不能對每個函式都“調到 11”,因為過於激進的最佳化實際上可能會損害效能或導致過長的構建時間。其他最佳化需要編譯器對函式中“常見”和“不常見”的路徑做出判斷。編譯器必須根據靜態啟發式方法進行最佳猜測,因為它無法知道在執行時哪些情況是常見的。

還是說,它能知道?

在沒有關於程式碼在生產環境中如何使用的確切資訊的情況下,編譯器只能操作包的原始碼。但是,我們有一個工具可以評估生產環境的行為:分析。如果我們向編譯器提供分析資訊,它就可以做出更明智的決定:更積極地最佳化最常使用的函式,或者更準確地選擇常見情況。

使用應用程式行為分析資訊進行編譯器最佳化被稱為基於配置檔案的最佳化 (Profile-Guided Optimization, PGO)(也稱為面向反饋的最佳化 (Feedback-Directed Optimization, 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,它會自動向伺服器新增一個 /debug/pprof/profile 端點,用於獲取 CPU 分析檔案。

通常,您希望從生產環境中收集分析檔案,以便編譯器獲得生產環境中行為的代表性檢視。由於此示例沒有“生產”環境,我們將建立一個簡單的程式來生成負載,同時收集分析檔案。將此程式的原始碼複製到 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,它將使用主包目錄中的 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 釋出!
部落格索引