原文在[這裡](https://go.dev/blog/pgo-preview)。 > 原文發佈於2023年2月8日 在構建Go二進位文件時,Go編譯器會進行優化,以儘可能生成性能最佳的二進位文件。例如,常量傳播可以在編譯時對常量表達式進行求值,避免了運行時的計算開銷;逃逸分析可以避免對局部作用域對 ...
原文在這裡。
原文發佈於2023年2月8日
在構建Go二進位文件時,Go編譯器會進行優化,以儘可能生成性能最佳的二進位文件。例如,常量傳播可以在編譯時對常量表達式進行求值,避免了運行時的計算開銷;逃逸分析可以避免對局部作用域對象進行堆分配,從而減少了垃圾回收的負擔;內聯則將簡單函數的代碼體複製到調用處,通常能夠進一步優化調用處的代碼(例如額外的常量傳播或更好的逃逸分析)。
Go在發佈的每個版本中都會改進優化,但這並不總是一項容易的任務。某些優化是可調節的,但編譯器不能對每個函數都進行過度激進的優化,因為過於激進的優化實際上可能會損害性能或導致過長的構建時間。其他優化要求編譯器對函數中的“常見”和“不常見”路徑進行判斷。編譯器必鬚根據靜態啟髮式規則進行最佳猜測,因為它無法在運行時知道哪些情況將是常見的。
但現在編譯器可以在運行時知道哪些情況是常見的了。
在沒有關於代碼在生產環境中如何使用的確切信息的情況下,編譯器只能對包的源代碼進行操作。但是我們確實有一種工具來評估生產行為:性能分析。如果我們向編譯器提供一個性能分析文件,它就可以做出更明智的決策:對最常用的函數進行更積極的優化,或更準確地選擇常見情況。
使用應用程式行為的性能分析文件進行編譯器優化的方法被稱為基於性能分析的優化(Profile-Guided Optimization,簡稱PGO,也被稱為反饋導向優化(Feedback-Directed Optimization,簡稱FDO))。
PGO/FDO通過收集和分析運行時的性能數據,使得編譯器能夠更準確地瞭解代碼的執行特性,從而進行更精細的優化。通過結合靜態分析和動態運行時數據,PGO/FDO可以產生更優化的代碼,提高程式的性能和效率。這種技術在提高大型複雜應用程式的性能方面非常有用,特別是對於高度頻繁執行的代碼路徑進行優化。
Go 1.20中包含了PGO的初步支持,作為預覽版本提供。請參閱profile-guided optimization user guide以獲取完整的文檔。儘管距離在生產環境中使用還有一段距離,但仍希望大家在工作中使用,並反饋遇到的問題或意見。
示例
以Markdown轉HTML服務為例:用戶通過/render
上傳Markdown文件,然後接收轉換後的HTML文件。這裡使用gitlab.com/golang-commonmark/markdown。
創建項目
$ go mod init example.com/markdown
$ go get gitlab.com/golang-commonmark/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 build -o markdown.nopgo
$ ./markdown.nopgo
2023/06/25 11:27:13 Serving on port 8080...
使用Go項目的README來進行測試:
$ curl -o README.md -L "https://raw.githubusercontent.com/golang/go/c16c2c49e2fa98ae551fc6335215fadd62d33542/README.md"
$ curl --data-binary @README.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>
...
<p>Note that the Go project uses the issue tracker for bug reports and
proposals only. See <a href="https://go.dev/wiki/Questions">https://go.dev/wiki/Questions</a> for a list of
places to ask questions about the Go language.</p>
性能分析
現在我們來採集一個profile文件,再使用PGO來重新構建服務,看看性能能提升多少。
在main.go
中,我們導入了net/http/pprof
包,它會自動為伺服器添加一個/debug/pprof/profile
地址,用於獲取CPU分析數據。
通常情況下,我們都是從生產環境中收集性能分析數據,以便編譯器能夠獲取在實際生產環境中的行為情況。但這個示例沒有一個真實的“生產”環境,我們將創建一個簡單的程式來生成負載,同時收集性能分析數據。將該程式的源碼複製到load/main.go
,並啟動負載生成器(確保伺服器仍在運行!)。
$ go run example.com/markdown/load
下載性能分析文件:
$ curl -o cpu.pprof "http://localhost:8080/debug/pprof/profile?seconds=30"
下載完成後,關閉服務。
啟用PGO
我們可以使用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
性能對比
我們將使用一個基於Go的基準測試版本的負載生成器來評估PGO對性能的影響。將這個基準測試的代碼複製到load/bench_test.go文件中。
首先沒有使用PGO的情況下進行測試:
$ ./markdown.nopgo
進行測試:
$ go test example.com/markdown/load -bench=. -count=100 -source ../README.md > nopgo.txt
然後啟用PGO:
$ ./markdown.withpgo
進行測試:
$ go test example.com/markdown/load -bench=. -count=100 -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) Core(TM) i7-6700HQ CPU @ 2.60GHz
│ nopgo.txt │ withpgo.txt │
│ sec/op │ sec/op vs base │
Load-8 445.1µ ± 4% 408.6µ ± 2% -8.21% (p=0.000 n=100)
新版本大約快了8.2%!在Go 1.20
中,通過啟用PGO,可以獲得2%到4%的CPU使用率提升。性能分析文件包含了關於應用程式行為的豐富信息,而Go 1.20
僅僅開始利用這些信息進行內聯優化。未來的發佈版本將繼續改進性能,因為編譯器的更多部分將利用PGO帶來的好處。
原文中效率提升了2.6%
文中的代碼可以在這裡找到。
聲明:本作品採用署名-非商業性使用-相同方式共用 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請註明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意