作者cppOrz (cppOrz)
看板C_and_CPP
標題[心得] 改善 C++ 編譯效率的方法
時間Fri Sep 2 00:17:03 2005
很久以前在 Exceptional C++ 上看過「編譯防火牆」的技術,
最近常用到這個東東。
C++ 似乎傾向把所有的負擔都推給 complier,而極力爭取設計
的靈活性及執行期的效率。這從語言本身許多豐富的機制可以
看出。
但是,如果專案的規模稍微大些,編譯所花去的時間就很可觀,
也許只是修改一個小地方的定義,卻牽一髮動全身,幾乎大部
份的檔案都要重新編譯過。
這裏討論一下改善編譯效率的幾個方法(除了改善硬體設備之
外)
首先,和語言無關的,最普通的方法就是把專案中很少變動的
模組以 dll/lib的方式獨立出來。當然,如果模組常常變化,
獨立成 lib 就沒啥好處,還容易造成錯誤。
另外,使用快取的功能或編譯器廠提供的特殊方法,也是有效
的手段。
至於,與 C++ 有關的特殊方法,其實只有一句話:
「將模組之間的依存性最小化」
如果在某個標頭檔(.h/.hpp)中 include 另外一個模組,
例如在 A 模組的標頭檔 include B 模組,那麼一旦 B 模組
的介面(.h 部份)有任何改變,不但 A 模組要重新編譯,
所有 include A 模組介面的其他模組,也必須重新編譯。
但如果 A 模組只是在實作檔(通常是.cpp) include B 模
組的介面,那麼 A 模組與 B 模組的介面依存性就降低了許
多,至少引用 A 模組介面的其他模組不必再重新編譯。
不要小看這一點,我常看到許多人寫程式,只求方便,想要
什麼功能,就一個一個 include 進來,其實有很多是非必要
的,或只要稍微花點小力氣,就可以降低依存性。這種習慣
在寫小程式或許完全沒差,但如果進行的是規模比較大的專案
,聚沙成塔,好習慣和壞習慣所累積的差異就會愈來愈明顯。
我曾經做過一個專案,我同事負責的部份,只要小小改動一
個定義檔,就要編譯超過半小時(如果整個重新編譯至少
一個小時以上是跑不掉的),為此我把他的專案照此原則簡
單的修改,移除了某些不必要的依存關係,小改動後再重編
的時間就節省了超過一半。
根據上述的原理,一個最簡單的降低模組依存性的法則:
「如果可以用前置宣告代替 include,就不要 include」
例如,有一個 Client 模組,在 Network 模組中會被用到,後
者有個介面如下:
void Send(Client *, char const *buf, int buflen);
因為在 Network 的介面中,只用到 Client 的指標,並不需要
參考到 Client 的定義,所以 include Client 模組就是多餘的
了,只需要一個前置宣告即可:
class Client;
這是最基本的手法。除了指標之外,如果介面只用到 reference
或是某些經過 extern 或 typedef 宣告(定義)出來的東西,
也可以不需要直接 include 整個模組,只要再次「聲明」一次
即可。C++ 標準程式庫中的 <iosfwd> 就是個好例子。(主要
由於不能直接動 namespace std 的主意)
◎編譯防火牆 Pimpl 手法
簡單描述一下 Exceptional C++ 所介紹的 Pimpl 手法:
class X
{
public :
... // pubilc interface
private :
class XImpl *Impl;
};
其實原理很簡單,就是把 X 的實作(private)的部份,委托給另
一個 class XImpl,這樣,不論 XImpl 怎麼實現,class X 的介面
都不會受到影響。如此一來,當 XImpl 的實現改變時,include X
介面的模組,就不需要重新編譯(只要重新連結)。
有一種中介軟體(例如:CORBA、COM……等等)的技術,就是遵循某
些介面設計的規則,把這種「只要介面不變,無論實作怎麼改變,用戶
都可以不用改變」的設計思維發揮到極限。雖然中介軟體的主要目的
在於分散式系統之間的物件互相溝通,但它的設計傾向,著重於二進
位碼的重用性,這和 C++ 極度傾向倚賴編譯器(也就是 C++ 原始碼
的移植性)的理念有著根本的不同。(當然,兩者的出發點和目的不
同,所以設計傾向當然不同)
不過,某些時候,當編譯效率不容忽視,那麼中介軟體的「遵循介面
設計法則」的精神,也就值得借鏡。
再舉個例子,由於 C++ Templates 的特性太過複雜,絕大多數的編譯
器廠都沒能實現 export 關鍵字的功能(我知道的只有 Comeau 和其他
極少數的 C++ Complier 有支援),造成 template 程式碼必須放在
標頭檔中,所以如果大量使用 template 機制,是很耗費編譯時間的。
不幸的是 C++ Standard,包括俗稱 STL 等常用的容器類,幾乎都是
template,而 namespace std 又不能拿來前置宣告,一旦要使用標準
容器,就只好「直接 include 進來」,例如:
// 檔案: MyClass.hpp
#include <vector>
class MyClass
{
public :
...
private :
...
std::vector<int> Container;
};
像這種設計是很常見的。當然,可以效法前面 Pimpl 的手法,來個
MyClassImpl,避免在 .hpp 檔 include vector 進來。但是,也許
MyClass private 的其他成員,大都是些基礎類型,或是某些不影響
編譯效率的物件,那為了避免在 .hpp 引入 vector,而整個委託給
MyClassImpl,實在是太過麻煩了!
◎Pimpl 手法的變型應用
因此,一個折衷的辦法就是不動其他的部份,只針對 vector 來處理
,以下是一個簡單的實現方法:
// 檔案: MyClass.hpp
class MyClass
{
public :
~MyClass();
MyClass();
...
private :
...
class Container *pContainer;
};
// 檔案: MyClass.cpp
#include <vector>
using std::vector;
struct MyClass::Container : vector<int> {};
MyClass::~MyClass()
{
delete pContainer;
}
MyClass::MyClass() :
pContainer(new Container)
{}
好了,經過小小的努力,使得在 .hpp 中引入 vector 的必要性解除
了。稍微檢討一下得失:
一、本來 vector 物件是配置在 stack 上,變成到 heap 上,效率
上相差不大(實際上配置在哪是要看 MyClass 的用戶),比較麻
煩的是 MyClass 不再適用 C++ 編譯器預設的 memberwise-copy
動作,也就是 copy ctor 和 copy assignment 必須自己實現(如
果需要的話)。
二、好處在哪裏?首先,vector 是 C++ Standard 的東西,它的介
面基本上可以當作不變。此外,直接在 .hpp 內引入 vector 似乎
也沒多慢……。關鍵在於,假如 MyClass 是個被大量引用的模組,
一旦它有任何改變,使用這種方法,就大大降低 vector 被直接曝
光的次數了(畢竟它的原始碼,包含其他引用模組的原始碼也不短)。
三、另一個好處是,可能 MyClass 的設計者後來發現,vector 不太
適當,也許換成 deque 或 list 較好,甚至考慮 multiset 或
multimap 等等(畢竟需求是常常在改變的,而設計考量的周密性
始終有限)。那麼這種設計的優點就充份展現出來了,因為不但
只要改動極少的程式碼(就是把原來繼承 vector 改成繼承其他
容器),更重要的是,整個 MyClass 的介面不變,其他引用到的
模組不必重新再編譯。
四、而且,即使 MyClass 介面的其他部份改變了,但 vector 這種
大量消耗編譯器分析及具現化時間的模組,沒有直接曝光,所有的
動作都只限於 MyClass 模組內部,其他模組不受影響,雖然要重
新編譯(因為 MyClass 介面的其他部份改變了),但速度也會快
得多。
當然,由於標準容器實在是太常用了,這種手法的好處平時並不太
明顯,不過如果儘量依照這種原則,將最常用、以及最耗費編譯時間
的幾個工具模組,採用這種方式製作,當專案的規模擴大時,它所帶
來的各種優勢(主要是編譯依存性的降低,編譯效率的改善),就會
愈來愈明顯。
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 59.120.214.120
推 gavintsou:推...140.116.118.146 09/02
推 Solccp:好文推 學到很多 59.113.169.89 09/02
推 adxis:好~! 218.172.20.87 09/02
推 renderer:推 61.222.148.171 09/02
推 pcjustin:推 219.91.106.83 09/02
推 bobhsiao:看不懂也要推~囧rz 218.169.37.81 09/02
※ 編輯: cppOrz 來自: 59.120.214.120 (09/02 22:30)