2015 年 9 月

第 30 卷,第 9 期

本文章是由機器翻譯。

編譯器最佳化 - 利用原生特性指引最佳化簡化程式碼

Hadi Brais | 2015 年 9 月

通常編譯器可以做出不正確的最佳化不實際改善程式碼的效能或,當它執行時更糟的是降低。前兩個發行項中所討論的最佳化作業是不可或缺的應用程式的效能。

本文涵蓋重要的技術稱為 「 特性指引最佳化 (選項 PGO) 可協助更有效率地最佳化程式碼編譯器後端。實驗的結果會顯示為 35%5%的效能改進。此外,當謹慎使用這項技術會永遠不會降低您的程式碼效能。

這篇文章是根據前兩個部分 (msdn.microsoft.com/magazine/dn904673msdn.microsoft.com/magazine/dn973015)。如果您剛接觸的 PGO 概念,我建議您先閱讀 Visual c + + 小組部落格張貼 bit.ly/1fJn1DI

PGO 簡介

其中一個最重要的最佳化編譯器會執行是函式內嵌。根據預設,Visual c + + 編譯器內嵌函式,只要呼叫端大小不會變得太大。許多函式呼叫會展開,不過,這只時很有用頻繁地進行呼叫。否則它只會增加程式碼,浪費空間指令和統一的快取的大小並增加應用程式工作集。怎麼編譯器知道呼叫是否頻繁地進行? 這最終取決於傳遞至函式的引數。

大部分最佳化缺乏可靠的啟發學習法進行明智的決策所需。我看過許多情況下造成顯著的效能降低的不正確的暫存器配置。當編譯程式碼,只要是希望所有的效能改進和降低從所有最佳化項目加總正速度結果。這幾乎都是如此,但它會產生極大的可執行檔。

那就天下太平了消除這種降低。如果您可以告知編譯器要如何在執行階段行為的程式碼,它可以最佳化程式碼。記錄有關程式行為的資訊在執行階段的程序稱為 「 程式碼剖析和產生的資訊即稱為 「 設定檔。您可以提供要使用它們來引導其最佳化編譯器的一或多個設定檔。這是 PGO 技術的需要。

您可以使用這項技術在原生和 managed 程式碼。不過,工具之間的差異因此這裡我將討論僅原生 PGO、 以及離開 PGO 另一篇文章。本章節的其餘部分會討論如何將 PGO 套用至應用程式。

PGO 是最好的方法。像其他項目,不過它有個缺點。花一些 (取決於應用程式大小) 的時間和精力。幸運的是,如稍後所見,Microsoft 會提供工具可大幅降低執行 PGO 至應用程式的時間。有三個階段來執行應用程式的 PGO — 檢測組建、 訓練課程和 PGO 組建。

檢測組建

有數種方式來分析執行中的程式。Visual c + + 編譯器會使用靜態的二進位檔檢測,這會產生最精確的設定檔,但所花費的時間。使用檢測,編譯器會插入少數的機器指令在您的程式碼的所有函式中感興趣的位置中所示 [圖 1。這些指示記錄何時會執行您的程式碼相關聯的組件和產生的設定檔中包含這項資訊。

的特性指引最佳化應用程式的檢測組建
圖 1] 的特性指引最佳化應用程式的檢測組建

有幾個步驟來建置應用程式的已檢測的版本。首先,您必須編譯使用 /GL 參數啟用整個程式最佳化 (WPO) 的所有原始程式檔。WPO 才能檢測程式 (不是技術上必要但它可以讓產生的設定檔也更實用)。將檢測只能以 /GL 的已編譯的檔案。

要盡可能順利的下一個階段,請避免使用額外的程式碼會導致任何編譯器參數。例如,停用內嵌函式 (/ Ob0)。此外,停用安全性檢查 (/ GS-) 和移除執行階段檢查 (沒有 /RTC)。這表示您不應該使用預設的版本和 Visual studio 偵錯模式。針對檔案以 /GL,不編譯速度最佳化 — 它們 (/ O2)。檢測的程式碼指定至少 /Og。

接下來,連結產生的目的檔和必要的靜態程式庫與 /ltcg: pgi 參數。這可讓連結器執行三項工作。它會告訴編譯器後端來檢測程式碼並產生 PGO 資料庫 (PGD) 檔案。這會用在第三個階段來儲存所有設定檔。在此時 PGD 檔案未包含任何設定檔。它只需要的資訊來識別用來偵測使用 PGD 檔案時是否已變更的目的檔。根據預設,PGD 檔案名稱會採用可執行檔的名稱。您也可以指定切換使用選擇性 /PGD 連結器 PGD 檔案名稱。第三項工作是連結 pgort.lib 匯入程式庫。可執行檔的輸出會相依於 PGO 執行階段 DLL pgortXXX.dll XXX 所在的 Visual Studio 版本。

這個階段的最終結果是可執行檔 (EXE 或 DLL) 檔案充斥著檢測程式碼和空白 PGD 檔案填滿和第三個階段中使用。您只能有靜態程式庫檢測如果該程式庫連結到所要檢測的專案。此外,相同版本的編譯器必須產生所有 CIL OBJ 檔案。否則連結器會發出錯誤。

程式碼剖析探查

再繼續進行下一個階段中,我想要討論的程式碼編譯器插入程式碼剖析程式碼。這可讓您估計加入至程式的額外負荷並了解在執行階段所收集的資訊。

若要錄製的設定檔,編譯器會插入探查的數字以 /gl 編譯每個函式中探查是小型的一連串的指示 (兩到四個指示) 數目的推播指示和結尾的探查處理常式一次呼叫指令所組成。在必要時探查包裝來儲存和還原所有 XMM 暫存器的兩個函式呼叫。有三種類型的探查:

  • 計數探查: 這是探查的最常見類型。它會計算程式碼區塊的遞增計數器每次執行時所執行的次數。它也是最便宜的大小和速度。每個計數器是在 x64 上的大小和 x86 上 4 個位元組中的 8 個位元組。
  • 項目探查: 編譯器會在每個函式的開頭插入項目探查。這個探查的目的是告訴其他探查中相同的函式來使用該函式相關聯的計數器。這被需要因為探查處理常式會探查跨函式之間共用。項目探查的 main 函式初始化 PGO 執行階段。項目探查也是計數探查。這是最慢的探查。
  • 值探查: 這些每個虛擬函式呼叫之前插入 switch 陳述式和用來記錄值的長條圖。因為它會計算每個值會出現次數值探查也是計數探查。這個探查的大小是最大。

如果它只需要一個基本區塊 (指示具有一個進入和離開的序列),將不會由任何探查檢測函式。事實上,可能內嵌雖然 /Ob0 參數。除了值探查每個 switch 陳述式會讓編譯器建立描述該常數 COMDAT 區段。此區段的大小會大約案例的數目乘以控制參數變數的大小。

每個探查結束這個處理常式的呼叫。項目探查的 main 函式會建立探查每個項目會指向不同的探查處理常式的處理常式指標 (在 x64 上的 8 個位元組和 x86 上 4 個位元組) 的向量。在大部分的情況下會有只有幾個探查處理常式。探查會插入每個函式中的下列位置:

  • 函式的進入點項目探查
  • 計數探查中最後呼叫的每個基本區塊或 ret 指令
  • 每個 switch 陳述式之前的值探查
  • 每個虛擬函式呼叫之前的值探查

因此記憶體額外負荷檢測程式數量取決於探查的數量、 所有 switch 陳述式中的案例數目、 switch 陳述式的數目和虛擬函式呼叫的數目。

所有探查一些點提前計數器記錄執行對應的程式碼區塊的其中一個處理常式。編譯器會使用相加指令 4 位元組計數器的增量一和 x64,攜帶加入計數器的高的 4 位元組的 ADC 指令上。這些指示不具備執行緒安全。這表示所有預設探查不是安全執行緒。如果至少其中一個函式可能同時執行一個以上的執行緒,結果將不會很可靠。在此情況下,您可以使用 /pogosafemode 連結器選項。這會導致編譯器為鎖定,讓所有的探查具備執行緒安全與這些指示的前置詞。當然,這可讓它們更慢也。不幸的是,您無法選擇性地套用這項功能。

如果您的應用程式是由多個專案的輸出是 EXE 或 DLL 檔案的 PGO 所組成,您必須針對每個重複此程序。

教育訓練階段

在第一個階段之後, 您有可執行檔和 PGD 檔案檢測的版本。第二個階段訓練、 可執行檔將會產生一或多個設定檔儲存在個別的 PGO 計數 (PGC) 檔。您會使用第三個階段中的這些檔案以最佳化程式碼。

這是最重要的階段是因為設定檔的精確度是整個程序的成功非常重要。會很有用的設定檔,它必須反映程式的常見使用案例。編譯器會最佳化假設紅褐色的案例常見的程式。如果這不是如此,您的程式可能會執行更糟的是欄位中。產生的常見使用案例的設定檔可協助編譯器知道速度最佳化最熱門的路徑和最佳化其大小、 冷的路徑中所示 [圖 2

建立 PGO 應用程式的教育訓練階段
[圖 2 建立的 PGO 應用程式的教育訓練階段

這個階段中的複雜程度取決於使用案例的數目和程式的本質。訓練課程很容易如果程式不需要任何使用者輸入。如果有許多使用案例,以循序方式產生每個案例的設定檔可能無法最快的方式。

在複雜的定型案例中所示 [圖 2, ,pgosweep.exe 是命令列工具,可讓您控制維護 PGO 執行階段執行時的設定檔的內容。您可以產生之程式的多個執行個體且同時適用於使用案例。

假設您有兩個處理程序 X 和 Y 中執行的執行個體。即將開始處理程序 X 一種情況時,呼叫 pgosweep 並將傳遞給它的處理序識別碼和 /onlyzero 切換。這會導致 PGO 執行階段以清除 [只在該程序的記憶體中設定檔的一部分。如果不指定的處理序識別碼,將會清除整個 PGC 設定檔。接著就可以開始案例。您可以初始化程序 Y 以類似的方式在兩個使用案例。

只有在程式的所有執行中執行個體終止時就會產生 PGC 檔案。不過,如果程式有很長的啟動時間和不想要執行的每一種情況,您可以強制執行階段產生設定檔並清除記憶體中設定檔以準備在同一個執行的另一種情況。這樣即可執行 pgosweep.exe 和傳遞處理序識別碼、 可執行檔名稱和 PGC 檔案名稱。

根據預設,PGC 檔案將會產生可執行檔相同目錄中。您可以將它變更時執行程式的第一個執行個體之前先設定 VCPROFILE_PATH 環境變數。

我在討論資料和指示的額外負荷檢測程式碼。在大部分的情況下可以容納這項負擔。PGO 執行階段預設記憶體耗用量將不會超過某個臨界值。如果開啟它需要更多記憶體,就會發生錯誤。在此情況下,您可以使用 VCPROFILE_ALLOC_SCALE 環境變數來引發該臨界值。

PGO 組建

只要您已執行所有的常見使用案例,您有一組可用來建置程式的最佳化的版本 PGC 檔案。您可以捨棄您不想要使用任何 PGC 檔案。

建置 PGO 版本的第一個步驟是使用稱為 pgomgr.exe 命令列工具合併所有 PGC 檔案。您也可以使用此編輯 PGD 檔案。若要合併的第一個階段中產生的 PGD 檔案兩個 PGC 檔案、 執行 pgomgr 並傳遞 /merge 交換器和 PGD 檔案名稱。這會將合併其名稱後面加上指定 PGD 檔案的名稱相符的目前目錄中的所有 PGC 檔案! # 和數字。編譯器和連結器可以使用產生的 PGD 檔案來最佳化程式碼。

您可以擷取較常見或重要的使用案例使用 pgomgr 工具。若要這樣做,傳遞對應 PGC 檔案名稱與 /merge:n 參數,其中 n 是一些要包含在 PGD 檔案中的正整數值表示 PGC 檔案的複本的數目。根據預設,n 是一位。這個多重性會導致偏差其偏好的最佳化的特定設定檔。

第二個步驟是執行連結器傳遞相同一組物件檔案如下所示第一階段。這次使用 /LTCG:PGO 參數。連結器會尋找 PGD 檔案與目前的目錄中的可執行檔的名稱相同。它會確保 CIL OBJ 檔並未變更自第一,階段中產生 PGD 檔案,然後將它傳遞給編譯器使用並最佳化程式碼。此處理序顯示於 [圖 3]。您可以使用 /PGD 連結器參數明確指定 PGD 檔案。不要忘記啟用函式內嵌此階段。

在三個階段中的 PGO 組建
[圖 3 在三個階段中的 PGO 組建

大多數的編譯器和連結器最佳化會變成特性指引。這個階段的最終結果是根據大小和速度的高度最佳化可執行檔。它是個不錯的主意現在測量效能提升。

維護程式碼基底

如果您要傳遞至連結器使用 /ltcg: pgi 參數的任何輸入檔進行任何變更,連結器將會拒絕指定 /LTCG:PGO 時使用 PGD 檔案。這是因為這類的變更會大幅影響 PGD 檔案的效益。

其中一個選項是重複產生另一個相容 PGD 先前所討論的三個階段。不過,如果變更是很小 (例如少量函式、 呼叫函式有點較低或有點更頻繁地或或許將新增不常用的功能) 然後又不可行重複整個程序。在此情況下,您可以使用 /LTCG:PGU 參數取代 /LTCG:PGO 參數。這會告知連結器,以略過 PGD 檔案的相容性檢查。

這些微小的變更將會累積一段時間。您將最後仍是很有幫助再次檢測應用程式。您可以決定當您已經達到這個點藉由查看編譯器輸出時 PGO 建立程式碼。它會告訴您 PGD 涵蓋的程式碼基底中有多少。如果設定檔的涵蓋範圍下降到低於 80%(如所示 [圖 4),最好一次檢測程式碼。不過,這個百分比會取決於應用程式的本質。

在動作中的 PGO

PGO 引導編譯器和連結器所採用的最佳化。我將使用 NBody 模擬器為了示範它的優點。您可以下載這個應用程式於 bit.ly/1gpEaCY。您必須一併下載及安裝 DirectX SDK 在 bit.ly/1LQnKge 編譯應用程式。

首先,我將應用程式編譯為在發行模式中的 PGO 版本比較它。若要建置 PGO 版本的應用程式,您可以使用 Visual Studio 的 [建置] 功能表的特性指引最佳化功能表項目。

您也應該啟用組合器使用 [c] /FA 編譯器參數輸出 (請勿為此示範使用 /FA [c] s)。此簡單的應用程式就足以訓練已檢測的應用程式一次產生一個 PGC 檔案並使用它來最佳化應用程式。這樣您就有兩個可執行檔: 其中一個盲目地最佳化和另一個 PGO。請確定您可以存取最終 PGD 檔案,因為稍後需要它。

現在您逐一執行其中一個這兩個可執行檔並比較 attained 的 GFLOP 計數,如果您會發現他們達成類似的效能。很顯然,要套用至應用程式的 PGO 是浪費時間。在進一步調查原來的應用程式大小已經 (適用於盲目地最佳化的應用程式) 降低從 531 KB 472 KB (PGO 型應用程式) 中或 11%。因此當您將 PGO 套用至此應用程式時,它減少了大小維持相同的效能。為什麼會這樣呢?

請考慮 DXUTParseCommandLine DXUT/核心/DXUT 從 200 列函式。CPP 檔案。藉由查看產生的組件程式碼的發行組建,您可以看到二進位程式碼的大小約為 2700 個位元組。相反地,PGO 組建中的二進位程式碼的大小是不能超過 1650 位元組。您可以查看下列迴圈的條件會檢查組譯碼指令從這項差異的原因:

for( int iArg = iArgStart; iArg < nNumArgs; iArg++ ) { ... }

盲目地最佳化的組建產生下列程式碼:

0x044 jge block1
; Fall-through code executed when iArg < nNumArgs
; Lots of code in between
0x362 block1:
; iArg >= nNumArgs
; Lots of other code

相反地,PGO 建置會產生下列程式碼:

0x043 jl   block1
; taken 0(0%), not-taken 1(100%)
block2:
; Fall-through code executed when iArg >= nNumArgs
0x05f ret  0
; Scenario dead code below
0x000 block1:
; Lots of other code executed when iArg < nNumArgs

許多使用者想要在 GUI 而不是傳入命令列中指定參數。所以這裡的常見案例的設定檔資訊所示迴圈永遠不會重複。沒有設定檔,就無法得知編譯器。所以它會繼續進行並積極地最佳化在迴圈內的程式碼。它會展開導致毫無意義的程式碼膨脹的許多功能。PGO 組建中,您可以提供的編譯器說迴圈的設定檔已絕對不會執行。藉此瞭解沒有點編譯器內嵌從迴圈主體的任何函式呼叫。

您可以看到組件程式碼片段與另一個有趣的差異。盲目地最佳化可執行檔中很少執行的分支是指示的改透過路徑中的條件式。幾乎一定會執行的分支是條件式指示從位於 800 個位元組。這不只會導致處理器分支預測器失敗,但也保證指令的快取遺漏。

PGO 組建來交換分支的分公司地點避免這兩個問題。事實上,便會移很少執行的分支到不同的區段中的可執行檔,藉此改善工作集的位置。這種最佳化稱為無作用程式碼分離。就會無法執行不使用設定檔。不常呼叫的函式,例如二進位碼的微小差異可顯著的效能差異。

當建置 PGO 程式碼,編譯器會顯示您多少函式的已編譯的所有已檢測的函式的速度。編譯器也會顯示這在 Visual Studio 輸出視窗中。不能超過百分之 10 的函式通常會編譯速度 (想像積極內嵌),而其餘部分都針對大小編譯 (的部分或完全不需要考慮內嵌)。

請考慮稍微更有趣函式 DXUTStaticWndProc,定義在相同的檔案。函式來控制結構看起來像是這樣:

if (condition1) { /* Lots of code */ }
if (condition2) { /* Lots of code */ }
if (condition3) { /* Lots of code */ }
switch (variable) { /* Many cases with lots of code in each */ }
if-else statement
return

盲目地最佳化的程式碼會發出如下所示的原始碼的相同順序的每個程式碼區塊。不過,PGO 組建中的程式碼已經過巧妙地重新排列使用的每個區塊並在其中執行每個區塊的時間執行頻率。很少執行前兩個條件,因此若要改善快取和記憶體使用量,對應的程式碼區塊是現在位於不同的區段。此外,辨識為落在 「 最忙碌路徑 (例如 DXUTIsWindowed) 中的函式會立即內嵌:

if (condition1) { goto dead-code-section }
if (condition2) { goto dead-code-section }
if (condition3) { /* Lots of code */ }
{/* Frequently executed cases pulled outside the switch statement */}
if-else statement
return
switch(variable) { /* The rest of cases */ }

大部分最佳化受益可靠的設定檔和其他人會變成可執行。如果 PGO 並不會產生顯著的效能改進,它一定會降低產生可執行檔和其記憶體系統的負荷的大小。

PGO 資料庫

PGD 設定檔的優點遠超過引導的編譯器最佳化。雖然您可以使用 pgomgr.exe 合併多個 PGC 檔案,它也可以做為不同的用途。它提供三個參數可讓您檢視有關紅褐色案例的程式碼的行為完全瞭解 PGD 檔案的內容。第一個參數 /summary 告訴工具来發出文字 PGD 檔案內容的摘要。搭配第一、 第二個參數 /detail 告訴工具来發出的詳細文字的設定檔描述。最後一個參數 / 唯一告訴快速 (尤其是適用於 c + + 程式碼基底) 的函式名稱工具。

以程式設計方式控制

還有一個值得一提的最後一項功能。Pgobootrun.h 標頭檔會宣告一個稱為 PgoAutoSweep 的函式。您可以呼叫此函式來以程式設計方式產生 PGC 檔案並清除記憶體中設定檔來準備進行下一個 PGC 檔案。此函數會採用一個引數的型別 char * 參照 PGC 檔案的名稱。若要使用此函式,您必須將 pgobootrun.lib 靜態程式庫連結。目前,這是與 PGO 相關的程式設計支援。

總結

PGO 是大小的有助於編譯器和連結器做出更佳最佳化藉由參考可靠的設定檔時與速度的取捨的最佳化技術需要解析。Visual Studio 提供視覺化存取這項技術透過 [建置] 功能表或專案的內容功能表。

不過,您可以取得豐富的功能使用 PGO 外掛程式,您可以從下載的 bit.ly/1Ntg4Be。這也詳細記載在 bit.ly/1RLjPDi。還記得從的涵蓋範圍臨界值 [圖 4, ,才能讓它最簡單方式是使用外掛程式文件中所述。不過,如果您偏好使用命令列工具,您可以參考的文件 bit.ly/1QYT5nO 提供充足的範例。如果您有原生程式碼基底,可能是個不錯的主意現在嘗試這項技術。當您這樣做,時請放心地讓我知道如何影響的大小和應用程式的速度。

PGO 程式碼基底的維護週期
[圖 4 PGO 程式碼基底的維護週期

其他資源

如特性指引最佳化資料庫的詳細資訊,請參閱 Hadi Brais 在部落格文章 bit.ly/1KBcffQ


Hadi Brais 是博士學者印度洋 institute 的技術 Delhi (IITD)、 研究下一代記憶體技術的編譯器最佳化作業。他也需要花費大部分時間撰寫程式碼在 C / C + + C# 和深入深入探索執行階段和編譯器架構。他的部落格網址 hadibrais.wordpress.com。與他連絡 hadi.b@live.com

感謝以下的微軟技術專家對本文的審閱: Ankit Asthana