一、爲什麼慢?
重要的一個原因是C++的基本 頭文件-源文件的編譯模型:
- 每個源文件爲一個編譯單元
- 頭文件數量多,可能會包含上百甚至上千個頭文件
- 存在重複解析,每個編譯單元中,這些頭文件都要從硬盤裏讀取然後被解析
- 每個編譯單元都會產生一個obj文件
- 這些obj文件被link到一起,此過程很難做到並行
二、如果加快編譯?
1. 代碼角度
1.1 使用前置聲明
不要直接包含頭文件,推薦使用前置聲明。
前置聲明只是在代碼中引入了類Foo,是一個不完全類型——因爲我們不知道它具體包含了哪些成員。
不完全類型只能在非常有限的情況下使用:
- 只能定義指向這種不完全類型的指針和引用(因爲不知道多大?)
- 只能聲明(但不可以定義)以不完全類型作爲參數或者返回類型的函數(也是因爲不知道多大?)
原因:
- 任何一個多餘的頭文件,都可能會被無限放大。
- 雖然很多時候前置聲明某個namespace中的類比較痛苦,但你仍值得這麼做
- 類的成員、函數參數等儘量使用引用、指針,爲前置聲明創造條件
壞處:
- 隱藏了依賴關係,頭文件改動時,用戶代碼會跳過必要的重編過程
- 前置聲明可能會被庫的後續更改破壞
- 前置聲明來自命名空間std::的symbol時,其行爲是未定義的
使用原則:
- 儘量避免前置聲明那些定義在其他項目中的實體
- 函數:總是使用#include
- 類模板:優先使用#include
1.2 使用Pimpl模式
全稱 Private Implemention
。
原因:
- 傳統的C++類接口與實現是放在一起的;Pimpl可實現二者完全分離
- 只要公共接口不變,對類實現的修改,始終只需要編譯此CPP,不涉及頭文件的重編
- 此外,對外界提供的頭文件內容也會精簡很多
1.3 高度模塊化
儘量低耦合,儘可能減少相互依賴。
原因:
- 文件與文件之間,一個文件的變化,儘量不要引起其他文件的重編
- 工程與工程之間,一個工程的修改,儘量不要引起其他工程的編譯
- 要求頭文件內容儘量單一,保證內聚性
- 優化思路:可以把代碼中最hot的頭文件找出來,拆分成高內聚的獨立小文件
1.4 刪除冗餘的頭文件
項目大了,又涉及團隊協作,很有必要通過自動化腳本識別和刪除冗餘的頭文件包含
1.5 注意inline和template
這兩種機制都會強制我們在頭文件中包含實現,會增加頭文件的內容。在使用這兩個新特性之前,需要仔細權衡一下。
2. 綜合技巧
2.1 預編頭文件(PCH)
**把一些常用的、但不常改動的頭文件放到預編譯頭文件中。 **
好處:在單個工程中,就不需要在每個編譯單元裏一遍又一遍的load與解析
2. 2 Unity Build
考慮將所有的CPP包含在一個CPP中(如all.cpp),然後只編譯all.cpp
好處:只存在一個編譯單元,不會重複的load與解析,同時只產生一個obj,鏈接時也不需要密集的磁盤操作。
2.3 ccache
全稱 compiler cache
,藉助上一次編譯的結果,使rebuild在保持結果相同的情況下,極大地提高速度。
好處:ccache是根據文件內容爲判斷原則,而非更新時間(git clone時不可靠)
2.4 避免過多的Additional Include Directories
編譯器在定位include的頭文件時,是根據你提供的include directories進行搜索。如果提供的目錄過多,則在搜索定位時耗費的時間就會增加。
3. 編譯資源
3.1 並行編譯
通過 make -j4
開啓4線程並行編譯。
3.2 升級磁盤
編譯速度比較依賴磁盤的讀寫性能,在多線程並行編譯時,磁盤性能可能存在瓶頸,可以升級更高轉數、或者SSD、或者RAID0
3.3 分佈式編譯
利用多態機器同時編譯,通常在比較大的工程上使用