Go 部落格

配置檔案引導最佳化預覽

Michael Pratt
2023年2月8日

當你構建一個 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,它自動為伺服器添加了一個 /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 後通常能獲得 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 釋出了!
部落格索引