一文讀懂Go 1.20引入的PGO效能最佳化

coding進階發表於2023-02-26

背景

Go 1.20版本於2023年2月份正式釋出,在這個版本里引入了PGO效能最佳化機制。

PGO的英文全稱是Profile Guided Optimization,基本原理分為以下2個步驟:

  • 先對程式做profiling,收集程式執行時的資料,生成profiling檔案。
  • 編譯程式時啟用PGO選項,編譯器會根據.pgo檔案裡的內容對程式做效能最佳化。

我們都知道在編譯程式的時候,編譯器會對程式做很多最佳化,包括大家熟知的內聯最佳化(inline optimization)、逃逸分析(escape analysis)、常數傳播(constant propagation)。這些最佳化是編譯器可以直接透過分析程式原始碼來實現的。

但是有些最佳化是無法透過解析原始碼來實現的。

比如一個函式里有很多if/else條件分支判斷,我們可能希望編譯器自動幫我們最佳化條件分支順序,來加快條件分支的判斷,提升程式效能。

但是,編譯器可能是無法知道哪些條件分支進入的次數多,哪些條件分支進入的次數少,因為這個和程式的輸入是有關係的。

這個時候,做編譯器最佳化的人就想到了PGO: Profile Guided Optimization。

PGO的原理很簡單,那就是先把程式跑起來,收集程式執行過程中的資料。然後編譯器再根據收集到的程式執行時資料來分析程式的行為,進而做針對性的效能最佳化。

比如程式可以收集到哪些條件分支進入的次數更多,就把該條件分支的判斷放在前面,這樣可以減少條件判斷的耗時,提升程式效能。

那Go語言如何使用PGO來最佳化程式的效能呢?我們接下來看看具體的例子。

示例

我們實現一個web介面/render,該介面以markdown檔案的二進位制格式作為輸入,將markdown格式轉換為html格式返回。

我們藉助 gitlab.com/golang-commonmark/markdown 專案來實現該介面。

環境搭建

$ go mod init example.com/markdown

新建一個 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 mod tidy
$ go build -o markdown.nopgo
$ ./markdown.nopgo
2023/02/25 22:30:51 Serving on port 8080...

程式主目錄下新建input.md檔案,內容可以自定義,符合markdown語法即可。

我演示的例子裡用到了input.md 這個markdown檔案。

透過curl命令傳送markdown檔案的二進位制內容給/render介面。

$ curl --data-binary @input.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>
...

可以看到該介面返回了input.md檔案內容對應的html格式。

Profiling

那接下來我們給main.go程式做profiling,得到程式執行時的資料,然後透過PGO來做效能最佳化。

main.go裡,有import net/http/pprof 這個庫,它會在原來已有的web介面/render的基礎上,新增一個新的web介面/debug/pprof/profile,我們可以透過請求這個profiling介面來獲取程式執行時的資料。

  • 在程式主目錄下,新增load子目錄,在load子目錄下新增main.go的檔案,load/main.go執行時會不斷請求上面./markdown.nogpo啟動的server的/render介面,來模擬程式實際執行時的情況。

    $ go run example.com/markdown/load
  • 請求profiling介面來獲取程式執行時資料。

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

​ 等待30秒,curl命令會結束,在程式主目錄下會生成cpu.pprof檔案。

注意:要使用Go 1.20版本去編譯和執行程式。

PGO最佳化程式

$ mv cpu.pprof default.pgo
$ go build -pgo=auto -o markdown.withpgo

go build編譯程式的時候,啟用-pgo選項。

-pgo既可以支援指定的profiling檔案,也可以支援auto模式。

如果是auto模式,會自動尋找程式主目錄下名為default.pgo的profiling檔案。

Go官方推薦大家使用auto模式,而且把default.pgo檔案也存放在程式主目錄下維護,這樣方便專案所有開發者使用default.pgo來對程式做效能最佳化。

Go 1.20版本里,-pgo選項的預設值是off,我們必須新增-pgo=auto來開啟PGO最佳化。

未來的Go版本里,官方計劃將-pgo選項的預設值設定為auto

效能對比

在程式的子目錄load下新增bench_test.go檔案,bench_test.go裡使用Go效能測試的Benchmark框架來給server做壓力測試。

未開啟PGO最佳化的場景

啟用未開啟PGO最佳化的server程式:

$ ./markdown.nopgo

開啟壓力測試:

$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > nopgo.txt

開啟PGO最佳化的場景

啟用開啟了PGO最佳化的server程式:

$ ./markdown.withpgo

開啟壓力測試:

$ go test example.com/markdown/load -bench=. -count=20 -source ../input.md > withpgo.txt

綜合對比

透過上面壓力測試得到的nopgo.txtwithpgo.txt來做效能比較。

$ go install golang.org/x/perf/cmd/benchstat@latest
$ benchstat nopgo.txt withpgo.txt
goos: darwin
goarch: amd64
pkg: example.com/markdown/load
cpu: Intel(R) Core(TM) i5-5250U CPU @ 1.60GHz
       │  nopgo.txt  │             withpgo.txt             │
       │   sec/op    │   sec/op     vs base                │
Load-4   447.3µ ± 7%   401.3µ ± 1%  -10.29% (p=0.000 n=20)

可以看到,使用PGO最佳化後,程式的效能提升了10.29%,這個提升效果非常可觀。

在Go 1.20版本里,使用PGO之後,通常程式的效能可以提升2%-4%左右。

後續的版本里,編譯器還會繼續最佳化PGO機制,進一步提升程式的效能。

總結

Go 1.20版本引入了PGO來讓編譯器對程式做效能最佳化。PGO使用分2個步驟:

  • 先得到一個profiling檔案。
  • 使用go build編譯時開啟PGO選項,透過profiling檔案來指導編譯器對程式做效能最佳化。

在生產環境裡,我們可以收集近段時間的profiling資料,然後透過PGO去最佳化程式,以提升系統處理效能。

更多關於PGO的使用說明和最佳實踐可以參考profile-guided optimization user guide

原始碼地址:pgo optimization source code

推薦閱讀

開源地址

文章和示例程式碼開源在GitHub: Go語言初級、中級和高階教程

公眾號:coding進階。關注公眾號可以獲取最新Go面試題和技術棧。

個人網站:Jincheng's Blog

知乎:無忌

福利

我為大家整理了一份後端開發學習資料禮包,包含程式語言入門到進階知識(Go、C++、Python)、後端開發技術棧、面試題等。

關注公眾號「coding進階」,傳送訊息 backend 領取資料禮包,這份資料會不定期更新,加入我覺得有價值的資料。還可以傳送訊息「進群」,和同行一起交流學習,答疑解惑。

References

相關文章