7.4 改善分支預測
6.2.2 節中,我們提到了二個改善 L1i 使用的方法,分別是藉由分支預測和重新排序執行區塊:藉由 __builtin_expect
進行靜態預測和狀態偵測的最佳化(PGO)。正確的分支預測對軟體性能有巨大的影響,但在這裡我們會著重在記憶體使用的改善狀況。
使用 __builtin_expect
(或 likely
和 unlikely
二個巨集)的方式非常簡單。只要把相關的定義放在中央標頭檔裡編譯器即可發揮作用。但仍有可能在需要使用 likely
的狀況卻使用 unlikely
,反之亦然。即便可用類似 oprofile 的工具來測量分支預測錯誤和 L1i 未命中,但這些問題仍不易排除。
雖然如此仍有一個簡單的方法可使用。A.2 節的程式碼另外定義了 likely
和 unlikely
二個巨集,可在程式執行階段主動測量靜態預測是否正確。且使用者或測試人員可就檢查結果進行調整。但測量不會考量程式的性能,只會測試程式設計師所做的靜態假設。詳細內容以及程式碼可在先前的章節中找到。
PGO 對 gcc 而言相當容易使用。只需三個步驟,但須滿足某些先決條件。首先所有來源檔案都須使用額外 -fprofile-generate
選項進行編譯。這個選項必須被傳遞到所有編譯器運行的程式或連結該程式的命令中。雖然可混合使用已啟用和未啟用此選項的檔案,但對沒有啟用此選項的目標檔案,PGO 不會被觸發。
開啟 PGO 後,除了編譯時間大幅增加外,編譯器產生的二進位檔案與尋常檔案無異,因為檔案記錄(和儲存)有關分支是否被採用等等相關資訊。編譯器還為每個輸入檔案新增一個名為 .gcno
的擴增檔案。這個檔案包含與程式碼中分支相關的資訊。這個檔案須保留以便後續使用。
當程式的二進位檔準備好時,應該用來執行某代表性工作集。不論哪種工作,二進位檔都會針對這項任務進行最佳化。程式的連續執行是可能且必要;每次的執行都會對同一個輸出檔做出貢獻。在程式結束之前,收集的資料會寫入 .gcda
擴增檔案中。這些檔案會在包含來源 (source) 檔案的目錄中新增。程式可從任何目錄執行,且二進位檔也可任意複製,但是來源目錄必須具備可使用且可寫入的權限。同樣地,為每個輸入的來源檔案新增一個輸出檔案。如果多次啟動程式,須要在來源目錄中找到之前執行所留下的 .gcda
檔案,否則後續執行資料就無法累積在同一個檔案中。
執行一組代表性測試後,就可重新編譯應用程式。編譯器必須能夠在包含來源檔案的同一目錄中找到 .gcda
檔。千萬不能移動這些檔案,否則編譯器會找不到,嵌入的檔案校驗也無法匹配。重新編譯時,要將 -fprofile-generate
參數替換為 -fprofile-use
,但原始碼更改有些限制。更改空格和注釋沒問題,但加上更多分支或程式碼區塊會使收集的資料無效,導致編譯失敗。
這些就是程式開發者該有的準備,不難吧?最重要是選擇最適合的工具來進行測量。如果測試工作不適合程式實際使用方式,執行結果可能會更糟。也因為如此使用 PGO 來進行函式庫最佳化非常困難。函式庫可在許多情境下使用,除非使用案例相似,否則最好使用 __builtin_expect
的靜態分支預測。
而對 .gcno
和 .gcda
二個檔案而言,有幾點需要注意。這些二進位檔案不能直接用來檢查。但可使用 gcc 套件中的 gcov 工具來進行檢視。這個工具主要用於覆蓋分析(因此得名),使用檔案格式與 PGO 相同。 gcov 工具為每個包含執行程式碼的來源檔案(可能包括系統標頭)生成 .gcov
擴增檔案。這些檔案依據給定的 gcov 參數,對原始程式碼標注 (annotate) 分支計數器和程式碼執行的機率等資訊。