歡迎光臨
每天分享高質量文章

Visual Studio 2017 性能提升和建議

來源:DevDivChina

blogs.msdn.microsoft.com/c/2018/01/26/visual-studio-2017-性能提升和建議/

隨著C++專案的壯大和優化器的日漸複雜,編譯器的編譯時間,或者說是性能,逐漸成為人們關註的焦點。這是我們Visual C++組非常關註的問題,也成為了15.5版本和未來工作的重點。我想花幾分鐘時間來為各位總結一下我們最近為提升性能所做的特殊的更改,並且可以為你提升編譯工程的性能提幾點建議。

這裡需要註意的是,並非所有的更改都可以提升所有場景的性能。把編譯時間降低到一個期望值是一個長遠的事業。最近我們開始把AAA游戲作為一個基準。未來還需要付出更多的努力。

VS工具集有三個部分需要分別改進。第一就是編譯器前端,也就是c1xx.dll的執行。這是一個將cpp檔案作為輸入並且生成一種不依賴於中間語言的語言的工具,或者IL,即將被輸入到編譯器後端的內容。編譯器後端也就是c2.dll。它從前端讀取IL並從中生成包含真正機器代碼的obj。最後是聯結器,它將讀取後端編譯器生成的各種objlib,並且將他們合併生成一個最終的二進制檔案。

編譯器前端性能

在許多工程里,前端編譯時間阻礙了整體性能的提升。幸運的是,通過直接給msbuild或者其他編譯系統加/MP選項(該選項可以使cl.exe同時處理多個檔案),或者甚至可以使用像incredibuild這樣的工具通過分佈式機器來加速。想要提升性能的第一步是在編譯工程時進行高效的分佈和並行。

第二步是確保你高效使用了PCH檔案。一個PCH檔案基本上是cl.exe 充分解析了.h檔案之後的記憶體轉儲解決了每次都需要這樣做的麻煩。你會被它的作用所震驚,頭檔案(比如windows.h或者一些DirectX頭)一旦被完全預處理會變得非常大,並且常常會成為一個後處理源檔案的最主要的部分。PCH檔案會開啟一個新世界。這裡的主要操作是只將會被頻繁更改的檔案包含進來,這就保證PCH會幫你改進很多性能。

最後一點建議是對#include的使用限制。在PCH檔案之外,包含一個檔案是一個非常昂貴的操作,這就牽扯到了在包含的路徑里搜索每一個檔案夾的問題。很多檔案的輸入輸出操作,這是一個每次都要被重覆的傳遞性操作。這就是PCH會起很大作用的原因。在微軟內部,人們有很多有關“只包含你要使用的檔案”的成功案例。/showInclude選項可以讓你認識到包含檔案是多麼的昂貴,並且可以指導你只包含你需要用的東西。

最後,我想讓你瞭解一下/Bt選項。這個選項可以顯示每個檔案的前端(同樣也包括後端和鏈接時間)編譯時間。它可以幫你查清性能低的原因,讓你知道哪個檔案需要你花時間去優化。

以下是我們為提升前端性能所做的更改。

掃清PGO計數

PGO,或者叫配置檔案優化,是一種編譯器後端科技,在微軟被廣泛使用。基本原理是你生成了一個特殊檢測版本的產品,通過運行測試用例生成配置檔案,基於已收集的資料進行重新編譯/優化。

我們發現在編譯或者優化前端二進制檔案(c1xx.dll)時使用的是舊的配置檔案資料。當我們重新檢測或者重新收集 PGO資料時,會看到10%的性能提升。

這裡我們學到的是,如果在產品中用PGO提升性能,請確保定期收集訓練過的資料。

移除_assume的使用

_assume(0)給編譯器後端傳遞一個信號, 告訴它一個特定的代碼路徑(也許是一個預設情況的標簽,等等)無法到達。許多產品會把它包含在一個宏里,並命名為類似於UNREACHABLE這樣的名字,然後執行,這樣debug版本會宣告這個信號,ship版本會把這個信號傳遞給編譯器。編譯器會做一些操作比如移除枝幹或者轉移標的宣告。

這是有道理的,如果在運行時一個_assume(0)宣告實際上是可到達的,結果就是生成錯誤代碼。這會在很多不同的情況下帶來很多問題(並且一些人抱怨這會引發安全問題)– 所以我們做了一個實驗來看看通過重新定義一個宏來簡單的移除所有的_assume(0)會帶來的影響。如果回歸很小,也許不值得把它放在產品中免得引發其他的問題。

令我們驚訝的是,移除_assume宣告使前端性能提升了1%-2%。這就很容易做決定了。這一現象的根源就是儘管在很多情況下_assume對應優化器是一個有效的信號,但實際上它也許會阻礙其他優化(尤其是比較新的優化)。在未來的版本中我們將持續對_assume進行改進。

改進winmd檔案加載

在winmd檔案加載問題上我們做了很多更改,旨在提升10%的加載性能(這一項大概占總編譯時間的1%)。這隻會影響UWP工程。

編譯器後端

編譯器後端包含了優化器。這裡有兩個等級的性能問題,“常規問題(在這裡我們做了大量的工作希望能有1-2%的提升),和長遠問題,這裡有一個特殊的方法會導致一些優化到達了一個不合理的路徑並且會花30s或者更長時間去編譯 — 但是大部分人沒被影響。我們關心這個並且一直為之改進。

如果你使用/Bt選項並且看到一個異常的檔案花費了非正常的時間做後端編譯,下一步就是在編譯時使用/d2cgsummary選項。Cgsummary(或者叫代碼生成概要)將會告訴你哪個函式花費的時間長。如果這個函式不在你的關鍵性能路徑中,說明你很幸運,那麼你就可以用以下的方法為該函式關閉優化:

#pragma optimize(“”, off)

void foo() {

}

#pragma optimize(“”, on)

那麼這個函式就不會被優化。和我們保持聯繫,我們可以幫你看看是否能修複這個問題。

除了為編譯時間不正常的方法關閉優化之外,我需要提示你,使用 _forceinline時要當心。通常客戶會使用forceinline讓行內器做他們想做的事情,這種情況下,我的建議是盡可能的有針對性的使用。編譯器後端會非常非常重視_forceinline。它會免除所有的行內預算檢查(_forceinline的花費不會對行內預算不利)。這些年我們看了許多案例,以代碼質量為由隨意使用_forceinline是性能提神的主要阻礙。基本的,不像其他編譯器,我們經常通過前端的IL行內預優化的方法。這樣做有時候有利,我們為不同的內容做不同的優化,但一個弊端是,很多工作我們將無法恢復。如果你有一個很深的行內樹,那麼這將很快變得無法控制。這就是碰到像是Tensorflow/libsodium這樣的地方編譯時間過長的根源。這是我們未來版本將要著眼改進的地方。

當使用LTCG build時請瞭解一下iLTCG。增量LTCG是一項新科技,使用它我們只需要對LTCG build中更改過的函式(以及該函式所依賴的函式,比如它的行內函式)做代碼生成。沒有它,即使只做了少量的更改,也要對整個二進制檔案重新代碼生成。如果你曾因為LTCG使你陷入內部開發迴圈而放棄使用它,請瞭解一下iLTCG

最後一點建議,也更加適用於LTCG build(這裡只有link.exe單獨做代碼生成而不是分佈cl.exe),考慮使用/cgtreads#來適應預設核心擴展策略。正如你以下將要看到的,為了更好的衡量我們做了一些更改,但預設的仍舊是使用4核。將來我們會著眼於增加預設核數,或者甚至可以靈活的適用機器的核數。

以下是我們近期為編譯器後端所做的免費的性能提升:

行內讀取快取

一些編譯器,通過將優化過的所有行內函式儲存在記憶體中來實現行內。這個時候,行內的執行,就是一個記憶體拷貝到當前函式的指定位置的問題。

然而在VC++中,我們對行內的操作有一些不同。我們實際上是從磁盤中重讀沒被優化的版本。這明顯會慢一些,但是同時可以減少記憶體的使用。在性能提升的問題上,這將是一個阻礙,尤其是對一個大量使用_forceinline的工程來說。

為了平衡記憶體和性能的問題,相對於其他編譯器,我們在記憶體問題上做了小的改動。編譯器後端在一個方法因為行內操作而被讀取了固定的次數之後會快取這個方法。實驗表明,當次數N=100時可以很好的平衡記憶體和性能問題。我們可以給編譯器傳遞引數/d2FuncCache#(或者在LTCG

Build時給聯結器傳遞引數/de:-FuncCache)來實現。0表示不快取,50表示inline執行50次時快取。

型別系統編譯提升

此項適用於LTCG buildLTCG build開始,後端會致力於編譯在所有型別程式中使用多種優化的模型,比如虛擬化。這很慢並且記憶體占用量大。以前,當碰到型別系統的問題時,我們建議人們通過給聯結器傳遞/d2:-noteypeopt來禁用它。最近,我們做了大量更改,為型別系統平衡這一問題。實際上這個更改非常基礎,這牽扯到我們如何執行bitset這一操作。

更好的擴展到多核

後端是多執行緒的。但是也有很多束縛:我們自下而上的執行編譯指令 – 意味著一個方法無論被呼叫過幾次都只編譯一次。這就是一個函式如果在被呼叫函式的編譯過程中使用收集到的信息做更好的優化。

這也存在一個限制:以上方法如果大小一定的情況下可以免除,並且可以在不適用自下而上的信息的情況下直接開始編譯。這樣就使得單執行緒編譯不會遭遇瓶頸,因為它會流失掉最終剩餘的少量的很大的方法,這類方法因為依賴樹很深而無法立即啟動。

我們已經重新評估了大容量函式的限制,並且明顯的降低了這一限制。我們這個更改,不對任意明顯的代碼質量流失做評價,但是這次性能的提升會大大取決於,這個工程之前究竟多大程度的被大容量的方法所限制。

其他行內提升

我們為行內時符號表的構造和合併做了更改。這一更改提供了一個全面的附加的很小的好處。

更好封鎖粒度的鎖操作

像大部分工程,我們持續不斷的配置和檢查鎖操作的限制。結果是,在少數情況下,我們提升了鎖的封鎖粒度,尤其是IL檔案如何被映射和得到以及符號如果互相映射。

符號表和符號映射的新的資料結構

LTCG時,準確的在模塊中映射符號需要做大量的工作。我們用新的資料結構重寫了這部分代碼並帶來了提升。這個尤其對整體型別有幫助,通常是游戲產業,這些工程里圖例的映射操作會非常大。

LTCG多執行緒的其他部分

編譯器後端是多執行緒的只是相對正確的說法。我們只談論後端的代碼生成部分,而撇開了主要工作不談。

然而,LTCG builds要複雜的多。它也包含一些其他部分。最近我們使其他部分多執行緒運作,代價是放棄了LTCG build 10%速度的提升。未來的版本中這項工作仍舊會持續。

聯結器的改進

如果你使用LTCG(並且你應該使用),你將可能看到聯結器是你編譯系統的一個阻礙。這有些不公平,因為LTCG時聯結器只用c2.dll做代碼生成 – 所以以上的建議都適用。但是除了代碼生成,聯結器有它傳統的工作要做,即解析取用和把所有obj生成一個最終的二進制檔案。

這裡你能做的最重要的事就是使用fastlink。Fastlink實際上是一個新的PDB格式,通過/debug:fastlink選項使用。鏈接時,這將極大的減少生成PDB所要做的工作。

在你的debug版本,你應該使用/increamental。增量鏈接允許聯結器只更新被更改的obj而不是rebuild整個二進制檔案。當你在內部迴圈做了一些更改,需要重新編譯鏈接測試重覆這些步驟的時候,增量鏈接會帶來很大的不同。和fastlink類似,我們在這裡做了大量的穩定的提升。假如你之前用過但是發現它不穩定,請重試一次。

以下是近期對編譯器性能提升所做的更改:

新的ICF heuristic

ICF-摺疊,是聯結器的重大阻礙之一。這是任何相同的函式為了節省空間而摺疊在一起的階段,也是這些函式的取用被重定向到單一實體的階段。

這個版本,對ICF做了一點更改。總結一下,通過依賴一個健壯的哈希函式來做對等代替使用memcmp。這將明顯加快ICF的速度。

回退到64位聯結器

32位聯結器對於大工程來說存在著地址空間的問題。它經常用獲取檔案的方式將檔案進行記憶體映射,假如檔案很大,需要相鄰地址空間的記憶體映射將不可能實現。作為備用方法,聯結器退回到速度較慢的快取I/O路徑,這裡聯結器只讀取它需要的部分檔案。

聯結器知道,與記憶體映射I/O相比,快取I/O代碼路徑會非常非常的慢。所以我們添加了一個新的邏輯,是的32位聯結器在退回到快取I/O之前將自己重啟為一個64位的行程。

Fastlink 改進

/debug:fastlink 在一定程度上是一個新的特征,它可以明顯的加速除錯信息生成,而這一步是所有鏈接實踐中一個主要的部分。我們建議所有人專攻這個選項,並且在任何可能的情況下使用它。在這個版本,我們加固並加速了這一選項,併在在未來的版本中給這個選項的編寫投入更多的時間和金錢來改進它。如果你最初使用過它,但是因為某一次不好的體驗而不再使用它,請再次嘗試。我們在15.6版本中對這個選項做了更多的提升。

增量連接的回退

我們聽到一個有關增量鏈接的抱怨,說有時候增量鏈接會比整體鏈接更慢,這取決於有多少objlib被更改。目前我們非常努力的查明這個狀況並且會直接的求助於整體鏈接。

結論

以上所列舉的內容並不詳盡,但是很好的總結了過去幾個月VS有關重大性能提升的更改。如果你曾經被VC++編譯鏈接時間過長所困擾,我建議你再次嘗試15.5的工具集。如果遇到一個工程和其他大小差不多的工程相比編譯時間毫無理由的過長,或者是跟其他工具相比時間過長,我們很願意幫你看一下。

請記得,cl.exe可以加/d2cgsummarylink.exe可以加/d2:-cgsummary,這兩個方法可以幫助你查明在生成代碼性能上的問題。這包含了以上所討論的行內器讀取快取的問題。當遇到大工程,記得加/Bt,它可以幫你查明每個檔案的前端後端編譯時間和鏈接時間。/time+可以顯示鏈接時間,包含了ICF使用的時間。


●編號118,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

Web開發

更多推薦18個技術類微信公眾號

涵蓋:程式人生、演算法與資料結構、黑客技術與網絡安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

赞(0)

分享創造快樂