配置檔案引導最佳化
從 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 編譯器期望 CPU pprof 配置檔案作為 PGO 的輸入。Go 執行時生成的配置檔案(例如來自 runtime/pprof 和 net/http/pprof)可以直接用作編譯器輸入。也可以使用/轉換來自其他分析系統的配置檔案。有關更多資訊,請參閱附錄。
為了獲得最佳結果,重要的是配置檔案要代表應用程式生產環境中的實際行為。使用不具代表性的配置檔案可能會導致生成的二進位制檔案在生產環境中幾乎沒有改進。因此,建議直接從生產環境收集配置檔案,這是 Go PGO 的主要設計方法。
典型的工作流程如下:
- 構建併發布初始二進位制檔案(不帶 PGO)。
- 從生產環境收集配置檔案。
- 當需要釋出更新的二進位制檔案時,從最新原始碼構建並提供生產配置檔案。
- 轉到 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)。
- 從生產環境收集配置檔案。
- 當需要釋出更新的二進位制檔案時,從最新原始碼構建並提供生產配置檔案。
- 轉到 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 構建嗎?
是的。配置檔案的格式在 OS 和架構配置之間是等效的,因此它們可以用於不同的配置。例如,從 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,前提是它們遵循以下一般要求:
-
其中一個樣本索引應具有型別/單位“samples”/“count”或“cpu”/“nanoseconds”。
-
樣本應表示樣本位置的 CPU 時間樣本。
-
配置檔案必須符號化(必須設定 Function.name)。
-
樣本必須包含行內函數的堆疊幀。如果省略行內函數,Go 將無法保持迭代穩定性。
-
必須設定 Function.start_line。這是函式起始的行號,即包含
func
關鍵字的行。Go 編譯器使用此欄位計算樣本的行偏移量(Location.Line.line - Function.start_line
)。請注意,許多現有 pprof 轉換器省略了此欄位。
注意:在 Go 1.21 之前,DWARF 元資料省略了函式起始行(DW_AT_decl_line
),這可能會使工具難以確定起始行。
有關特定第三方工具的 PGO 相容性的更多資訊,請參閱 Go Wiki 上的 PGO 工具頁面。