如何提高 C++ 項目的編譯速度

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 CC 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)化依賴關系

  1. 避免循環(huán) include,通常如果出現(xiàn)這種現(xiàn)象,應該重新組織文件布局,使得項目高度模塊化。
  2. 使用條件編譯語句可以避免重復 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。
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容