背景
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.txt
和withpgo.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 領取資料禮包,這份資料會不定期更新,加入我覺得有價值的資料。還可以傳送訊息「進群」,和同行一起交流學習,答疑解惑。