加快C++編譯速度的方法

編譯速度慢的原因

因為C++ .h + .cpp 的編譯模型
每個cpp檔可能會包含上百甚至上千個.h檔,這些.h檔都會被讀進來一遍,然後被解析一遍。
每個編譯單元都會產生一個.obj文件,然後所以這些.obj文件會被link到一起,並且這個過程很難平行。重複load與解析,以及密集的IO,使編譯速度很慢。

代碼角度

前置聲明

.h檔中使用前置聲明(forward declaration),而不是直接包含.h檔。

1
2
3
4
5
class A; //forward declaration, instead #include "a.h"
class B{
A* a;
void useA(A& a);
};
1
2
3
4
#include "a.h" //cannot use forward declaration because compiler needs to know what A really is
class B{
A a;
};

要盡一切可能使.h檔精簡
很多時候前置聲明某個namespace中的class會比較痛苦,而直接include會方便很多,千萬要抵制住這種誘惑;class的成員,函數參數等也儘量用reference或pointer。

使用Pimpl模式

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

1
2
3
4
5
6
7
8
9
10
11
class A
{
public:
A();
~A();
void doSomething();

private:
struct Impl;//real implementation in this class
std::auto_ptr<impl> m_impl;
};

高度模塊化

模塊化就是低耦合,就是儘可能的減少相互依賴。

  1. 文件與文件之間,一個.h檔的變化,儘量不要引起其他文件的重新編譯。
  2. 工程與工程之間,對一個工程的修改,儘量不要引起太多其他工程的編譯。這就要求.h檔,或者工程的內容一定要單一,不要什麼東西都往裡面塞,從而引起不必要的依賴。

不要把兩個不相關的class,或者沒什麼聯繫的macro定義放到一個.h檔裡;內容要儘量單一。

把代碼中最常用到的那些.h檔找出來,然後分成多個獨立的小文件,效果相當可觀。

刪除冗餘的header檔

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

特別注意inline和template

它們強制在.h檔中包含實作,這會增加.h檔的內容,從而減慢許多編譯速度,需權衡使用。

預編譯.h

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

首次編譯source.cpp時,編譯器生成header.pch的預編譯header。以後再編譯該程式時,編譯器會比較該表頭檔的時間戳,如果表頭檔沒有改變,編譯器直接使用預編譯header。

1
2
3
4
5
CORE_PCH_FILENAME=Core.h
CORE_PCH=$(CORE_PCH_FILENAME).gch

$(CORE_PCH):
$(CXX) $(CXX_CFLAGS) -x c++-header $(CORE_PCH_FILENAME)

Guard Conditions

保證每個 header file 在每個編譯單元只被 include 一次

1
#pragma once
1
2
3
4
#ifndef filename_h
#define filename_h
//...
#endif

同時使用兩種方法以確保compiler的相容性

Unity Build

把所有的檔案包含到一個cpp中(如all.cpp),然後只編譯all.cpp。這樣就只有一個編譯單元,這意味著不需要重複load與解析同一個.h檔了,同時因為只產生一個obj文件,在link的時候也不需要那麼密集的IO

Compiler Cache

藉由快取上一次編譯的結果,使rebuild在保持結果相同的情況下,極大的提高速度。

不要有太多的Include Directories

編譯器定位你include的.h檔,是根據你提供的include directories進行搜索的。

cpp -v 查看 #include "..." search starts here: 中的目錄
和 GNU Make 的 -I 選項

平行化及分佈式編譯

GNU Make 的 -j [N] 可以用N個核心編譯
Visual Studio 有 /MP 選項可做到檔案等級的平行
或是用空閒的機器來編譯

買更好的磁碟

編譯速度慢很大一部分原因是磁碟操作,那麼除了儘可能的減少磁碟操作,我們還可以做的就是加快磁碟速度。

參考資料

  • What techniques can be used to speed up C++ compilation times?
  • 如何加快C++代碼的編譯速度
  • (Unity Build) CppCon 2014: Nicolas Fleury "C++ in Huge AAA Games"
  • (Unity Build) Is Ubisoft's unity build for C++ worth?
  • (pimpl) C++: 善用 PIMPL 技巧
  • (去除重複) Perl腳本,用來自動去除這些冗餘的.h檔
  • (預編譯.h檔) 終於搞懂了,預編譯header 檔(precompiled header)
  • (分佈式編譯) Xoreax IncrediBuild
  • How do I know the default include directories
  • (Compiler Cache) ccache
  • (預編譯.h檔) Speed up C++ compilation, part 1: precompiled headers
  • (預編譯.h檔) makefile 範例