檔案導向最佳化
從 Go 1.20 開始,Go 編譯器支援檔案導向最佳化 (PGO) 以進一步最佳化建置。
目錄
概觀
收集剖析資料
使用 PGO 建置
注意事項
常見問題
附錄:替代剖析資料來源
概觀
剖析引導最佳化 (PGO),也稱為回饋導向最佳化 (FDO),是一種編譯器最佳化技術,它會將應用程式代表性執行結果的資訊 (剖析資料) 回饋給編譯器,以用於下一次編譯應用程式,而編譯器會使用這些資訊來做出更明智的最佳化決策。例如,編譯器可能會更積極地內嵌剖析資料中指出經常呼叫的函式。
在 Go 中,編譯器使用 CPU pprof 剖析資料作為輸入剖析資料,例如來自 runtime/pprof 或 net/http/pprof。
截至 Go 1.22,針對一組具代表性的 Go 程式進行的基準測試顯示,使用 PGO 建置可將效能提升約 2-14%。我們預期隨著後續版本的 Go 中有更多最佳化利用 PGO,效能提升幅度將會持續增加。
收集剖析資料
Go 編譯器預期輸入 PGO 的資料是 CPU pprof 剖析資料。Go 執行時期產生的剖析資料 (例如來自 runtime/pprof 和 net/http/pprof) 可直接用作編譯器輸入。也可能可以使用/轉換來自其他剖析系統的剖析資料。請參閱 附錄 以取得更多資訊。
為獲得最佳結果,重要的是剖析檔案必須代表應用程式在生產環境中的實際行為。使用非代表性的剖析檔案可能會導致二進位檔案的生產力幾乎沒有改善。因此,建議直接從生產環境收集剖析檔案,而這是 Go 的 PGO 主要設計的方法。
典型的作業流程如下
- 建置並釋出一個初始二進位檔案(沒有 PGO)。
- 從生產環境收集剖析檔案。
- 當需要釋出一個更新的二進位檔案時,從最新的來源建置並提供生產剖析檔案。
- GOTO 2
Go PGO 通常對於應用程式的剖析版本與建置剖析檔案的版本之間的偏差很強健,對於使用已最佳化的二進位檔案收集的剖析檔案建置也很強健。這使得這個反覆的作業週期成為可能。請參閱 AutoFDO 部分以取得有關這個作業流程的其他詳細資料。
如果從生產環境收集很困難或不可能(例如,分發給最終使用者的命令列工具),也可以從代表性的基準收集。請注意,建構代表性的基準通常很困難(在應用程式演進時保持其代表性也很困難)。特別是,微基準通常不適合 PGO 剖析,因為它們只執行應用程式的一小部分,當應用到整個程式時,產生的效益很小。
使用 PGO 建置
建置的標準方法是在剖析二進位檔案的主要套件目錄中儲存一個 pprof CPU 剖析檔案,其檔案名稱為 default.pgo
。預設情況下,go build
會自動偵測 default.pgo
檔案並啟用 PGO。
建議直接在來源儲存庫中提交剖析檔案,因為剖析檔案是建置的輸入,對於可重製(且效能良好!)的建置很重要。與來源一起儲存可簡化建置體驗,因為除了擷取來源之外,不需要其他步驟來取得剖析檔案。
對於更複雜的場景,go build -pgo
旗標控制 PGO 剖析選擇。此旗標預設為 -pgo=auto
,用於上面說明的 default.pgo
行為。將旗標設定為 -pgo=off
會完全停用 PGO 最佳化。
如果您無法使用 default.pgo
(例如,一個二進位檔的不同場景有不同的剖析、無法將剖析儲存在來源中,等等),您可以直接傳遞要使用的剖析路徑(例如,go build -pgo=/tmp/foo.pprof
)。
注意:傳遞給 -pgo
的路徑適用於所有主要套件。例如,go build -pgo=/tmp/foo.pprof ./cmd/foo ./cmd/bar
將 foo.pprof
套用於二進位檔 foo
和 bar
,這通常不是您想要的。通常不同的二進位檔應該有不同的剖析,透過個別的 go build
呼叫傳遞。
注意:在 Go 1.21 之前,預設值為 -pgo=off
。必須明確啟用 PGO。
注意事項
從生產環境收集具代表性的剖析
您的生產環境是為您的應用程式收集具代表性剖析的最佳來源,如 收集剖析 中所述。
開始執行的最簡單方法是將 net/http/pprof 新增到您的應用程式,然後從服務的任意執行個體擷取 /debug/pprof/profile?seconds=30
。這是入門的好方法,但有可能是無代表性的
-
這個執行個體在剖析時可能沒有執行任何動作,即使它通常很忙碌。
-
流量模式可能會在一天中改變,導致行為在一天中改變。
-
執行個體可能會執行長時間運作(例如,5 分鐘執行運作 A,然後 5 分鐘執行運作 B,等等)。30 秒的剖析可能只會涵蓋單一運作類型。
-
執行個體可能無法公平分配要求(某些執行個體接收的某種類型要求多於其他執行個體)。
更穩健的策略是從不同的執行個體收集多個不同時間的剖析,以限制個別執行個體剖析之間差異的影響。然後可以將多個剖析合併成單一剖析,以供 PGO 使用。
許多組織執行「持續剖析」服務,自動執行此類全艦隊抽樣剖析,然後可將其用作 PGO 的剖析來源。
合併剖析
pprof 工具可以像這樣合併多個剖析
$ go tool pprof -proto a.pprof b.pprof > merged.pprof
此合併實際上是輸入中範例的直接總和,與剖析的牆面持續時間無關。因此,當剖析應用程式的短時間片段(例如,無限期執行的伺服器)時,您可能希望確保所有剖析具有相同的牆面持續時間(即,所有剖析都收集 30 秒)。否則,牆面持續時間較長的剖析將在合併的剖析中被過度表示。
AutoFDO
Go PGO 設計為支援「AutoFDO」樣式的流程。
讓我們仔細看看收集剖析中所述的流程
- 建置並釋出一個初始二進位檔案(沒有 PGO)。
- 從生產環境收集剖析檔案。
- 當需要釋出一個更新的二進位檔案時,從最新的來源建置並提供生產剖析檔案。
- GOTO 2
這聽起來似乎很簡單,但這裡有一些重要的屬性需要注意
-
開發持續進行中,因此剖析的二進制檔案(步驟 2)的原始碼可能與正在建置的最新原始碼(步驟 3)略有不同。Go PGO 設計為對此具有穩健性,我們稱之為來源穩定性。
-
這是一個封閉迴圈。也就是說,在第一次迭代後,二進位檔案的剖析版本已經使用前一次迭代的剖析資料進行 PGO 最佳化。Go PGO 也設計為對此具有穩健性,我們稱之為迭代穩定性。
來源穩定性是使用啟發法來比對剖析資料中的範例與編譯來源。因此,許多對來源程式碼的變更,例如新增函式,不會對比對現有程式碼造成影響。當編譯器無法比對變更的程式碼時,會遺失一些最佳化,但請注意,這是一種優雅的劣化。單一函式無法比對可能會失去最佳化機會,但整體而言,PGO 的好處通常會擴及許多函式。請參閱來源穩定性區段,以取得有關比對和劣化的更多詳細資料。
迭代穩定性是防止在連續的 PGO 建置中出現效能變化的循環(例如,建置 #1 很快速,建置 #2 很慢,建置 #3 很快速等)。我們使用 CPU 剖析資料來找出要使用最佳化處理的熱門函式。理論上,熱門函式可以透過 PGO 大幅加速,以致於在下次剖析資料中不再顯示為熱門函式,也不會進行最佳化,使其再次變慢。Go 編譯器對 PGO 最佳化採取保守的方法,我們相信這可以防止顯著的差異。如果您觀察到這種不穩定性,請在 go.dev/issue/new 提交問題。
來源和迭代穩定性共同消除了對兩階段建置的要求,其中第一個未最佳化的建置會作為指標進行剖析,然後使用 PGO 重新建置以進行生產(除非絕對需要最佳效能)。
來源穩定性和重構
如上所述,Go 的 PGO 會盡力嘗試繼續比對舊剖析資料中的範例與目前的來源程式碼。特別是,Go 使用函式內的行偏移量(例如,在函式 foo 的第 5 行呼叫)。
許多常見的變更不會中斷比對,包括
-
熱函數以外檔案的變更(在函數上方或下方新增/變更程式碼)。
-
將函數移至同一套件中的另一個檔案(編譯器完全忽略來源檔名)。
可能會中斷比對的某些變更
-
熱函數內的變更(可能會影響行偏移)。
-
重新命名函數(和/或方法的類型)(變更符號名稱)。
-
將函數移至另一個套件(變更符號名稱)。
如果剖析相對較新,則差異可能只影響少數熱函數,限制無法比對的函數中錯失最佳化的影響。不過,由於程式碼很少會重構回舊形式,因此效能劣化會隨著時間慢慢累積,所以定期收集新的剖析以限制生產環境的來源偏差非常重要。
剖析比對可能大幅劣化的一種情況是大規模重構,重新命名許多函數或在套件間移動函數。在這種情況下,你可能會在新的剖析顯示新結構之前,遭遇短期的效能損失。
對於機械式的重新命名,理論上可以改寫現有的剖析,將舊的符號名稱變更為新的名稱。 github.com/google/pprof/profile 包含以這種方式改寫 pprof 剖析所需的原語,但截至撰寫本文時,還沒有現成的工具可以做到這一點。
新程式碼的效能
新增新程式碼或透過標記翻轉啟用新的程式碼路徑時,該程式碼在第一次建置時不會出現在剖析中,因此在收集反映新程式碼的新剖析之前,不會收到 PGO 最佳化。評估新程式碼的推出時,請記住初始版本不會代表其穩態效能。
常見問題
是否可以使用 PGO 來最佳化 Go 標準函式庫套件?
可以。Go 中的 PGO 套用於整個程式。所有套件都會重新建置以考量潛在的剖析引導最佳化,包括標準函式庫套件。
是否可以使用 PGO 來最佳化相依模組中的套件?
可以。Go 中的 PGO 套用於整個程式。所有套件都會重新建置以考量潛在的剖析引導最佳化,包括相依項中的套件。這表示應用程式使用相依項的獨特方式會影響套用於該相依項的最佳化。
使用非代表性剖析的 PGO 會讓我的程式比不使用 PGO 慢嗎?
不應該。雖然非代表性生產行為的剖析會導致應用程式冷門部分的最佳化,但它不應該讓應用程式的熱門部分變慢。如果你遇到 PGO 導致效能比停用 PGO 更差的程式,請在 go.dev/issue/new 提交問題。
我可以在不同的 GOOS/GOARCH 建置中使用相同的剖析嗎?
可以。剖析的格式在不同的作業系統和架構設定中是相同的,因此它們可以在不同的設定中使用。例如,從 linux/arm64 二進位檔收集的剖析可以用於 windows/amd64 建置。
話雖如此,上面討論的原始碼穩定性注意事項也適用於此。這些設定中不同的任何原始碼都不會最佳化。對於大多數應用程式而言,絕大多數程式碼都是與平台無關的,因此這種形式的降級是有限的。
舉個具體的例子,os
套件中檔案處理的內部在 Linux 和 Windows 之間有所不同。如果這些函式在 Linux 剖析中很熱門,Windows 等效項將不會獲得 PGO 最佳化,因為它們與剖析不符。
你可以合併不同 GOOS/GOARCH 建置的剖析。請參閱下一個問題以了解這樣做的權衡。
我應該如何處理用於不同工作負載類型的單一二進位檔?
這裡沒有明顯的選擇。用於不同類型工作負載的單一二進位檔(例如,在一個服務中大量讀取資料庫,而在另一個服務中大量寫入資料庫),可能會有不同的熱門元件,從不同的最佳化中受益。
有三個選項
-
為每個工作負載建立不同的二進位檔版本:使用每個工作負載的設定檔來建立多個針對工作負載的二進位檔版本。這將為每個工作負載提供最佳效能,但可能會增加處理多個二進位檔和設定檔來源的作業複雜性。
-
僅使用「最重要的」工作負載的設定檔來建立單一二進位檔:選取「最重要的」工作負載(最大的佔用空間、效能最敏感),並僅使用該工作負載的設定檔來建立。這會為選定的工作負載提供最佳效能,而且對於其他工作負載而言,由於最佳化了工作負載間共用的共用程式碼,效能可能仍有適度的改善。
-
合併所有工作負載的設定檔:取得每個工作負載的設定檔(依總佔用空間加權),並將它們合併成單一的「艦隊範圍」設定檔,用於建立單一的共用設定檔以進行建立。這可能會為所有工作負載提供適度的效能改善。
PGO 如何影響建立時間?
啟用 PGO 建立可能會導致套件建立時間顯著增加。最顯著的元件是,PGO 設定檔套用於二進位檔中的所有套件,表示第一次使用設定檔時需要重建依賴關係圖中的每個套件。這些建立會像其他建立一樣快取,因此使用相同設定檔的後續增量建立不需要完全重建。
如果您遇到建立時間大幅增加的情況,請在 go.dev/issue/new 提出問題。
注意:編譯器解析設定檔也會增加顯著的負擔,特別是對於大型設定檔。對於大型依賴關係圖使用大型設定檔會大幅增加建立時間。這由 go.dev/issue/58102 追蹤,並將在未來的版本中解決。
PGO 如何影響二進制大小?
由於額外的函式內嵌,PGO 可能導致二進制檔案略微變大。
附錄:替代剖析資料來源
Go 執行時期產生的 CPU 剖析(透過 runtime/pprof 等)已經採用正確的格式,可直接用作 PGO 輸入。不過,組織可能偏好使用其他工具(例如 Linux perf),或現有的全艦隊持續剖析系統,並希望與 Go PGO 搭配使用。
如果將其他來源的剖析轉換為 pprof 格式,則可以使用 Go PGO,前提是符合下列一般需求
-
其中一個範例索引應具有類型/單位「範例」/「計數」或「CPU」/「奈秒」。
-
範例應表示範例位置的 CPU 時間範例。
-
剖析必須具有符號(Function.name 必須設定)。
-
範例必須包含內嵌函式的堆疊框架。如果省略內嵌函式,Go 將無法維持反覆穩定性。
-
Function.start_line 必須設定。這是函式起點的行號。亦即,包含
func
關鍵字的行。Go 編譯器使用此欄位計算範例的行偏移量(Location.Line.line - Function.start_line
)。請注意,許多現有的 pprof 轉換器會省略此欄位。
注意:在 Go 1.21 之前,DWARF 元資料會省略函式起點行(DW_AT_decl_line
),這可能會讓工具難以判斷起點行。
請參閱 Go Wiki 上的 PGO 工具 頁面,以取得特定第三方工具的 PGO 相容性相關資訊。