C++ 項目的編譯時間一般可以從以下幾個角度進行優(yōu)化:
- 使用 Pimpl 模式
- 移動語義替代復制語義
- 前向聲明替代 include
- 優(yōu)化依賴關系
- 預編譯頭文件技術(PCH)
- 謹慎使用 inline 和 template
- 不改代碼:借助硬件性能
使用 Pimpl 模式
聲明與實現(xiàn)分離,接口定義要相對穩(wěn)定,include 時只 include 這個接口頭文件就好。
Pimpl(Pointer to Implementor),顧名思義就是將真正的實現(xiàn)細節(jié)的 Implementor 從類定義的頭文件中分離出去,公有類通過一個私有指針指向隱藏的實現(xiàn)類,是促進接口和實現(xiàn)分離的重要機制。
通常的 Pimpl 的手法是在 API 的頭文件中提供接口類的定義以及實現(xiàn)類的前置聲明,實現(xiàn)類的本身定義和成員函數(shù)的實現(xiàn)都隱藏在 cpp 文件中去,同時為了避免實現(xiàn)類的符號污染外部名字空間,實現(xiàn)類大多作為接口類的內部嵌套類的形式。
不使用 Pimpl 手法,代碼會像這樣:
#include <x.h>
class C {
public:
void f1();
private:
X x; // 與 X 的強耦合
};
從語義上來說,成員數(shù)據 x 是屬于類 C 的實現(xiàn)部分,不應該暴露給用戶。
從實現(xiàn)上來說,在用戶的代碼中,每一次使用 new C 和 C c1 這樣的語句,都會將類 X 的大小硬編碼到編譯后的二進制代碼段中(如果類 X 有虛函數(shù),則還不止這些)——這是因為,對于 new C 這樣的語句,其實相當于 operator new(sizeof(C)) 后面再跟上類 C 的構造函數(shù),而 C c1 則是在當前棧上騰出 sizeof(C) 大小的空間,然后調用類 C 的構造函數(shù)。因此,每次類 X 作了改動,使用 c.h 的源文件都必須重新編譯一次,因為類 X 的大小可能改變了。
而使用 Pimpl 手法的代碼像這樣:
#include <x.h>
class C {
...
private:
X* pimpl; // 這時 class X 不用完全定義, 因為任何指針的大小都是相同的, 這個大小已經確定了
};
當用戶 new C 或者 C c1 的時候,編譯器生成的代碼中不會摻雜類 X 的任何信息。
移動語義替代復制語義
當一個函數(shù)的參數(shù)按值傳遞時,就會進行拷貝。對于基本數(shù)據類型,編譯器懂得如何去拷貝。 而對于自定義的類型,我們需要提供拷貝構造函數(shù)。
但不得不說,拷貝的代價是昂貴的。所以我們需要尋找一個避免不必要拷貝的方法,即 C++11 提供的移動語義。移動語義一般是通過右值引用的方式實現(xiàn)的。
在 C++ 中臨時變量在表達式結束后就被銷毀,之后程序就無法再引用這個變量了。但 C++ 11 提供了一個方法,讓我們可以引用這個臨時變量。這個方法就是所謂的右值引用。
移動構造函數(shù)類似于拷貝構造函數(shù),把類的實例對象作為參數(shù),并創(chuàng)建一個新的實例對象。但是移動構造函數(shù)可以避免內存的重新分配,因為我們知道右值引用提供了一個暫時的對象,我們可以把它當做一個臨時存儲。這就意味著我們要移動而不是拷貝右值參數(shù)的內容。從而節(jié)省很多的時間和空間。
如下是一個移動賦值運算符語法示例:
A& operator=(A&& other) {
if (this != &other) {
// 釋放已有資源
delete[] mData;
mData = other.mData;
// 釋放源對象指針
other.mData = NULL;
}
return *this;
}
前向聲明替代 include
刪除不必要的 include,使用前向聲明(forward declaration)替代。
C++ 的類可以進行前向聲明。但是,僅僅進行前向聲明而沒有定義的類是不完整的,這樣的類,只能用于定義指針、引用,以及用于函數(shù)形參的指針和引用。不能用于定義對象(因為此時編譯器只知道這是個類,還不知道這個類的大小有多大),也不能訪問類的對象。
如 Pimpl 模式一節(jié)的例子可以將 #include <x.h> 替換為前向聲明:
class X; // 前向聲明, 類 X 的定義在 x.h 文件中
class C {
...
private:
X* pimpl; // 這時 class X 不用完全定義, 因為任何指針的大小都是相同的, 這個大小已經確定了
};
優(yōu)化依賴關系
- 避免循環(huán) include,通常如果出現(xiàn)這種現(xiàn)象,應該重新組織文件布局,使得項目高度模塊化。
- 使用條件編譯語句可以避免重復 include,但絕大多數(shù) C++ 編譯器在遇到這個語句時仍會把"打開文件并讀取文件內容然后進行字符串掃描"這套動作做一遍。出于這個角度,我們也應該主動優(yōu)化依賴關系,減少編譯過程中所需的 I/O 量。
預編譯頭文件(PCH)技術
要使用預編譯頭,我們必須指定一個頭文件,這個頭文件包含我們不會經常改變的代碼和其他的頭文件,用一個 .cpp 包含它并編譯生成一個 .pch 文件(GCC 下直接 g++ x.h 即可,生成的預編譯頭后綴為 .gch,不同后綴的預編譯頭不兼容),在接下來編譯到 include 這個頭文件的代碼時,就直接使用預編譯頭文件了,速度的提升相當明顯。
這些預先編譯好的代碼可以是任何的 C/C++ 代碼,甚至是 inline 函數(shù),但是必須是穩(wěn)定的,在工程開發(fā)的過程中不會被經常改變。如果這些代碼被修改,則需要重新編譯生成預編譯頭文件。
【注意】使用預編譯頭技術造成了一個問題:
由于它假定預編譯頭中包含過的頭文件會在所有 .cpp 中使用,因此它在編譯你的 .cpp 的時候,就會將預編譯頭中已經編譯完的部分加載到內存中。如果它突然發(fā)現(xiàn)你的 .cpp 居然沒有包含預編譯頭,整個編譯過程就會失敗,因為它不知道該如何將已編譯完的部分從內存中請出去。
因此,如果你使用了預編譯頭技術,就必須在所有的 .cpp 中包含預編譯頭。stdafx.h 是 MFC 工程提供的一個默認的預編譯頭。
謹慎使用 inline 和 template
在頭文件中 使用 inline 和 template 的話會強制我們包含實現(xiàn),從而了增加頭文件的內容,減慢編譯速度。使用之前,權衡一下。
避免 inline 復雜函數(shù),不過其實即使你聲明了 inline,編譯器也是自己看著來,所以一般不需特別注意。
根據標準,template 是不經過預編譯,直接從源文件導入,在工程編譯的時候編譯的。比如所有接受數(shù)據類型名為參數(shù)的 template,幾乎必須是在編譯的時候才能確定到底是什么,然后直接展開到源代碼里面去。舉個例子,寫個簡單的max函數(shù):
template <typename tname>
tname max(tname a, tname b){
return a > b? a: b;
}
如果這個 max 函數(shù)用在了 double 和 int 兩種類型上,那么實際上就會編譯出兩個 max 函數(shù)。而整個 STL 就是一大堆模板混來混去,你用一個 vector,就至少帶了一個 less 方法,即使你用不到。
不改代碼:借助硬件性能
- Visual studio 和 make 都支持并行編譯;
- 使用分布式編譯工具,如 IncrediBuild;
- 用固態(tài)硬盤;
- 添加編譯緩存 ccache。