如何加快C++代碼的編譯速度

來源:互聯網
上載者:User

   C++代碼一直以其運行時的高效能高調面對世人, 但是說起編譯速度,卻只有低調的份了。比如我現在工作的原始碼,哪怕使用Incredibuild調動近百台機子,一個完整的build也需要四個小時,恐怖!!!雖然平時開發一般不需要在本地做完整的build,但編譯幾個相關的工程就夠你等上好一段時間的了(老外管這個叫monkey around,相當形象)。想想若干年在一台單核2.8GHZ上工作時的情境 - 面前放本書,一點build按鈕,就低頭讀一會書~~~往事不堪回首。

  可以想象,如果不加以重視,編譯速度極有可能會成為開發過程中的一個瓶頸。那麼,為什麼C++它就編譯的這麼慢呢?

  我想最重要的一個原因應該是C++基本的"標頭檔-源檔案"的編譯模型:

  每個源檔案作為一個編譯單元,可能會包含上百甚至上千個標頭檔,而在每一個編譯單元,這些標頭檔都會被從硬碟讀進來一遍,然後被解析一遍。

  每個編譯單元都會產生一個obj檔案,然後所以這些obj檔案會被link到一起,並且這個過程很難並行。

  這裡,問題在於無數標頭檔的重複load與解析,以及密集的磁碟操作。

  下面從各個角度給出一些加快編譯速度的做法,主要還是針對上面提出的這個關鍵問題。

  一、代碼角度

  在標頭檔中使用前置聲明,而不是直接包含標頭檔。

  不要以為你只是多加了一個標頭檔,由於標頭檔的"被包含"特性,這種效果可能會被無限放大。所以,要盡一切可能使標頭檔精簡。很多時候前置申明某個namespace中的類會比較痛苦,而直接include會方便很多,千萬要抵制住這種誘惑;類的成員,函數參數等也盡量用引用,指標,為前置聲明創造條件。

  使用Pimpl模式

  Pimpl全稱為Private Implementation。傳統的C++的類的介面與實現是混淆在一起的,而Pimpl這種做法使得類的介面與實現得以完全分離。如此,只要類的公用介面保持不變,對類實現的修改始終只需編譯該cpp;同時,該類提供給外界的標頭檔也會精簡許多。

  高度模組化

  模組化就是低耦合,就是儘可能的減少相互依賴。這裡其實有兩個層面的意思。一是檔案與檔案之間,一個標頭檔的變化,盡量不要引起其他檔案的重新編譯;二是工程與工程之間,對一個工程的修改,盡量不要引起太多其他工程的編譯。這就要求標頭檔,或者工程的內容一定要單一,不要什麼東西都往裡面塞,從而引起不必要的依賴。這也可以說是內聚性吧。

  以標頭檔為例,不要把兩個不相關的類,或者沒什麼聯絡的宏定義放到一個標頭檔裡。內容要盡量單一,從而不會使包含他們的檔案包含了不需要的內容。記得我們曾經做過這麼一個事,把代碼中最"hot"的那些標頭檔找出來,然後分成多個獨立的小檔案,效果相當可觀。

  其實我們去年做過的refactoring,把眾多DLL分離成UI與Core兩個部分,也是有著相同的效果的 - 提高開發效率。

  刪除冗餘的標頭檔

  一些代碼經過上十年的開發與維護,經手的人無數,很有可能出現包含了沒用的標頭檔,或重複包含的現象,去掉這些冗餘的include是相當必要的。當然,這主要是針對cpp的,因為對於一個標頭檔,其中的某個include是否冗餘很難界定,得看是否在最終的編譯單元中用到了,而這樣又可能出現在一個編譯單元用到了,而在另外一個編譯單元中沒用到的情況。

  之前曾寫過一個Perl指令碼用來自動去除這些冗餘的標頭檔,在某個工程中竟然去掉多達了5000多個的include。

  特別注意inline和template

  這是C++中兩種比較"先進"的機制,但是它們卻又強制我們在標頭檔中包含實現,這對增加標頭檔的內容,從而減慢編譯速度有著很大的貢獻。使用之前,權衡一下。

  二、綜合技巧

  先行編譯標頭檔(PCH)

  把一些常用但不常改動的標頭檔放在先行編譯標頭檔中。這樣,至少在單個工程中你不需要在每個編譯單元裡一遍又一遍的load與解析同一個標頭檔了。

  Unity Build

  Unity Build做法很簡單,把所有的cpp包含到一個cpp中(all.cpp) ,然後只編譯all.cpp。這樣我們就只有一個編譯單元,這意味著不需要重複load與解析同一個標頭檔了,同時因為只產生一個obj檔案,在連結的時候也不需要那麼密集的磁碟操作了,估計能有10x的提高,看看這個視頻感受一下其做法與速度吧。

  ccache

  compiler cache, 通過cache上一次編譯的結果,使rebuild在保持結果相同的情況下,極大的提高速度。我們知道如果是build,系統會對比原始碼與目標代碼的時間來決定是否要重新編譯某個檔案,這個方法其實並不完全可靠(比如從svn上拿了上個版本的代碼),而ccache判斷的原則則是檔案的內容,相對來講要可靠的多。很可惜的是,Visual Studio現在還不支援這個功能 - 其實完全可以加一個新的命令,比如cache build,介於build與rebuild之間,這樣,rebuild就可以基本不用了。

  不要有太多的Additional Include Directories

  編譯器定位你include的標頭檔,是根據你提供的include directories進行搜尋的。可以想象,如果你提供了100個包含目錄,而某個標頭檔是在第100個目錄下,定位它的過程是非常痛苦的。組織好你的包含目錄,並盡量保持簡潔。

  三、編譯資源

  要提高速度,要麼減少任務,要麼加派人手,前面兩個方面講得都是減少任務,而事實上,在提高編譯速度這塊,加派人手還是有著非常重要的作用的。

  並行編譯

  買個4核的,或者8核的cpu,每次一build,就是8個檔案並行著編,那速度,看著都爽。 要是你們老闆不同意,讓他讀讀這篇文章:Hardware is Cheap, Programmers are Expensive

  更好的磁碟

  我們知道,編譯速度慢很大一部分原因是磁碟操作,那麼除了儘可能的減少磁碟操作,我們還可以做的就是加快磁碟速度。比如上面8個核一塊工作的時候,磁碟極有可能成為最大的瓶頸。買個15000轉的磁碟,或者SSD,或者RAID0的,總之,越快越好。

  分布式編譯

  一台機子的效能始終是有限的,利用網路中閒置cpu資源,以及專門用來編譯的build server來協助你編譯才能從根本上解決我們編譯速度的問題,想想原來要build 1個多小時工程的在2分鐘內就能搞定,你就知道你一定不能沒有它 - Incredibuild並行,其實還可以這麼做。

  這是一個比較極端的情況,如果你用了Incredibuild,對最終的編譯速度還是不滿意,怎麼辦?其實只要跳出思維的架構,編譯速度還是可以有質的飛躍的 - 前提是你有足夠多的機器:

  假設你有solution A和solution B,B依賴於A,所以必須在A之後Build B。其中A,B Build各需要1個小時,那麼總共要2個小時。可是B一定要在A之後build嗎?跳出這個思維架構,你就有了下述方案:

  同時開始build A和B 。

  A的build成功,這裡雖然B的build失敗了,但都只是失敗在最後的link上。

  重新link B中的project。

  這樣,通過讓A的build與B的編譯並行,最後link一下B中的project,整個編譯速度應該能夠控制在1個小時15分鐘之內。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.