精華區beta C_and_CPP 關於我們 聯絡資訊
很久以前在 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)