天天看點

如何分析和提高大型項目(CC++)的編譯速度?

如何分析和提高大型項目(C/C++)的編譯速度?

如何分析和提高大型項目(CC++)的編譯速度?

C/C++編譯基本原理

對于C/C++代碼通常來說整個建構過程分為以下幾個主要部分:

·預處理

在此階段主要完成的工作是将頭檔案展開、替換宏指令、條件編譯展開、消除注釋。

·編譯

在此階段主要将預編譯好的檔案轉換成彙編語言(進階語言->LLVM平台無關語言->平台彙編語言)。

·彙編

在此階段将彙編語言轉換為二進制機器語言。

·連結

将編譯産物和預編譯制品(.o、.a、.so)“拼”成可執行檔案,具體一些就是為main編譯過程中每一個未定義的符号去編譯産物中挨個尋找相應的實作代碼,補全符号位址資訊。

在編譯耗時分析中也就應該對以上幾個主要方面分别進行時間次元的評估,逐漸細化分析粒度确定時間瓶頸,直到某個檔案、某個函數、某個模闆才能有針對性地制定從宏觀的建構系統到微觀的檔案、符号的具體優化方案。

預處理

gcc -E選項可以得到預處理後的結果,擴充名為.i或 .ii。一般來說對預處理階段的分析尤為重要,因為預處理完了之後的中間檔案才是真正編譯過程的輸入。預處理後檔案的體量大小直接影響了後續階段各個部分的時間消耗水準。由于C/C++編譯特點的影響,每個編譯單元(源檔案),都需要獨立解析所有包含的頭檔案。也就是說如果N個源檔案引用到了同一個頭檔案,則這個頭檔案需要解析N次。如果頭檔案中有模闆(STL/Boost),則該模闆在每個cpp檔案中使用時都會做一次執行個體化,N個源檔案中的std::vector會執行個體化N次。通常來說預處理階段資源消耗是否合理一般可以通過以下幾個具體名額進行檢視。

·單個源檔案頭檔案引用總數

·單個頭檔案被引用總數

編譯

gcc -S選項可以得到編譯後的彙編代碼檔案,擴充名為.s。在該階段中,GCC為了滿足使用者不同程度的的優化需要,提供了近百種優化選項,用來對編譯時間,目标檔案長度,執行效率這個三維模型進行不同的取舍和平衡。優化的方法不一而足,總體上将有以下幾類:

.精簡操作指令。

.盡量滿足CPU的流水操作。

.通過對程式行為地猜測,重新調整代碼的執行順序。

.充分使用寄存器。

.對簡單的調用進行展開等等。

而使用編譯器提供的優化選項有時也可能需要再别的地方做取舍。部分優化選項會精簡指令改變代碼執行順序,這會導緻程式的可調試性大幅降低。另外一些涉及對寄存器、記憶體進行優化的指令可能會使程式在記憶體或者寄存器中結果的正确性得不到保障,這也是偶爾會在涉及到寄存器操作的代碼中看到Volatile關鍵字的原因之一。

為了簡化使用者操作,GCC也提供了相應的一些預設優化方案例如O0~03

·O0:不做任何優化,這是預設的編譯選項。

·O和O1:對程式做部分編譯優化,編譯器會嘗試減小生成代碼的尺寸,以及縮短執行時間,但并不執行需要占用大量編譯時間的優化。

·O2:是比O1更進階的選項,進行更多的優化。GCC将執行幾乎所有的不包含時間和空間折中的優化。當設定O2選項時,編譯器并不進行循環展開以及函數内聯優化。與O1比較而言,O2優化增加了編譯時間的基礎上,提高了生成代碼的執行效率。

·O3:在O2的基礎上進行更多的優化,例如使用僞寄存器網絡,普通函數的内聯,以及針對循環的更多優化。

·Os:主要是對代碼大小的優化,通常各種優化都會打亂程式的結構,讓調試工作變得無從着手。并且會打亂執行順序,依賴記憶體操作順序的程式需要做相關處理才能確定程式的正确性。

彙編

gcc -c選項可以得到彙編後的結果檔案,擴充名為.o。.o檔案,是按照的二進制編碼方式生成的檔案。在這個階段中可以做的優化方案并不多,是以暫時隻需要了解該階段的基本作用即可。

連結

簡單的講,連結器的工作就是解析未定義的符号引用,将目标檔案中的占位符替換為符号的位址。連結器還要完成程式中各目标檔案的位址空間的組織,這可能涉及重定位工作。在C./C++程式的連結過程中主要涉及以下角色:

·靜态庫:指編譯連結時,把庫檔案的代碼全部加入到可執行檔案中,是以生成的檔案比較大,但在運作時也就不再需要庫檔案了,其字尾名一般為“.a”。

·動态庫:在編譯連結時并沒有把庫檔案的代碼加入到可執行檔案中,而是在程式執行時由運作時連結檔案加載庫,這樣可執行檔案比較小,動态庫一般字尾名為“.so”。

·可執行檔案:将所有的二進制檔案連結起來融合成一個可執行程式,不管這些檔案是目标二進制檔案還是庫二進制檔案。

C++編譯特性

編譯單元

C/C++的編譯系統和其他進階語言存在很大的差異,其他進階語言中,編譯單元是整個Module,即Module下所有源碼,會在同一個編譯任務中執行。而在C/C++中,編譯單元是以檔案為機關。每個.c/.cc/.cxx/.cpp源檔案是一個獨立的編譯單元,導緻編譯優化時隻能基于本檔案内容進行優化,很難跨編譯單元提供代碼優化。

頭檔案解析

如果N個源檔案引用到了同一個頭檔案,則這個頭檔案需要解析N次。

如果頭檔案中有模闆(STL/Boost),則該模闆在每個cpp檔案中使用時都會做一次執行個體化,N個源檔案中的std::vector會執行個體化N次。

模闆函數執行個體化

在C++ 98語言标準中,對于源代碼中出現的每一處模闆執行個體化,編譯器都需要去做執行個體化的工作;而在連結時,連結器還需要移除重複的執行個體化代碼。顯然編譯器遇到一個模闆定義時,每次都去進行重複的執行個體化工作,進行重複的編譯工作。此時,如果能夠讓編譯器避免此類重複的執行個體化工作,那麼可以大大提高編譯器的工作效率。在C++ 0x标準中一個新的語言特性–外部模闆的引入解決了這個問題。

在C++ 98中,已經有一個叫做顯式執行個體化(Explicit Instantiation)的語言特性,它的目的是訓示編譯器立即進行模闆執行個體化操作(即強制執行個體化)。而外部模闆文法就是在顯式執行個體化指令的文法基礎上進行修改得到的,通過在顯式執行個體化指令前添加字首extern,進而得到外部模闆的文法。

.顯式執行個體化文法:template class vector。

.外部模闆文法:extern template class vector。

一旦在一個編譯單元中使用了外部模闆聲明,那麼編譯器在編譯該編譯單元時,會跳過與該外部模闆聲明比對的模闆執行個體化。