如何分析和提高大型項目(CC++)的編譯速度?
如何分析和提高大型項目(C/C++)的編譯速度?
C/C++編譯基本原理
對于C/C++代碼通常來說整個構建過程分為以下幾個主要部分:
·預處理
在此階段主要完成的工作是將頭文件展開、替換宏指令、條件編譯展開、消除注釋。
·編譯
在此階段主要將預編譯好的文件轉換成匯編語言(高級語言->LLVM**無關語言->**匯編語言)。
·匯編
在此階段將匯編語言轉換為二進制機器語言。
·鏈接
將編譯產物和預編譯制品(.o、.a、.so)“拼”成可執(zhí)行文件,具體一些就是為main編譯過程中每一個未定義的符號去編譯產物中挨個尋找相應的實現(xiàn)代碼,補全符號地址信息。
在編譯耗時分析中也就應該對以上幾個主要方面分別進行時間維度的評估,逐漸細化分析粒度確定時間瓶頸,直到某個文件、某個函數(shù)、某個模板才能有針對性地制定從宏觀的構建系統(tǒng)到微觀的文件、符號的具體優(yōu)化方案。
預處理
gcc -E選項可以得到預處理后的結果,擴展名為.i或 .ii。一般來說對預處理階段的分析尤為重要,因為預處理完了之后的中間文件才是真正編譯過程的輸入。預處理后文件的體量大小直接影響了后續(xù)階段各個部分的時間消耗水平。由于C/C++編譯特點的影響,每個編譯單元(源文件),都需要**解析所有*含的頭文件。也就是說如果N個源文件引用到了同一個頭文件,則這個頭文件需要解析N次。如果頭文件中有模板(STL/Boost),則該模板在每個cpp文件中使用時都會做一次實例化,N個源文件中的std::vector會實例化N次。通常來說預處理階段資源消耗是否合理一般可以通過以下幾個具體指標進行查看。
·單個源文件頭文件引用總數(shù)
·單個頭文件被引用總數(shù)
編譯
gcc -S選項可以得到編譯后的匯編代碼文件,擴展名為.s。在該階段中,**C為了滿足用戶不同程度的的優(yōu)化需要,提供了近百種優(yōu)化選項,用來對編譯時間,目標文件長度,執(zhí)行效率這個三維模型進行不同的取舍和平衡。優(yōu)化的方法不一而足,總體上將有以下幾類:
.精簡操作指令。
.盡量滿足CPU的流水操作。
.通過對程序行為地猜測,重新調整代碼的執(zhí)行順序。
.充分使用寄存器。
.對簡單的調用進行展開等等。
而使用編譯器提供的優(yōu)化選項有時也可能需要再別的地方做取舍。部分優(yōu)化選項會精簡指令改變代碼執(zhí)行順序,這會導致程序的可調試性大幅降低。另外一些涉及對寄存器、內存進行優(yōu)化的指令可能會使程序在內存或者寄存器中結果的正確性得不到保障,這也是偶爾會在涉及到寄存器操作的代碼中看到Volatile關鍵字的原因之一。
為了簡化用戶操作,**C也提供了相應的一些預設優(yōu)化方案例如O0~03
·O0:不做任何優(yōu)化,這是默認的編譯選項。
·O和O1:對程序做部分編譯優(yōu)化,編譯器會嘗試減小生成代碼的尺寸,以及縮短執(zhí)行時間,但并不執(zhí)行需要占用大量編譯時間的優(yōu)化。
·O2:是比O1更高級的選項,進行更多的優(yōu)化。**C將執(zhí)行幾乎所有的不*含時間和空間折中的優(yōu)化。當設置O2選項時,編譯器并不進行循環(huán)展開以及函數(shù)內聯(lián)優(yōu)化。與O1比較而言,O2優(yōu)化增加了編譯時間的基礎上,提高了生成代碼的執(zhí)行效率。
·O3:在O2的基礎上進行更多的優(yōu)化,例如使用偽寄存器網絡,普通函數(shù)的內聯(lián),以及針對循環(huán)的更多優(yōu)化。
·Os:主要是對代碼大小的優(yōu)化,通常各種優(yōu)化都會打亂程序的結構,讓調試工作變得無從著手。并且會打亂執(zhí)行順序,依賴內存操作順序的程序需要做相關處理才能確保程序的正確性。
匯編
gcc -c選項可以得到匯編后的結果文件,擴展名為.o。.o文件,是按照的二進制編碼方式生成的文件。在這個階段中可以做的優(yōu)化方案并不多,所以暫時只需要了解該階段的基本作用即可。
鏈接
簡單的講,鏈接器的工作就是解析未定義的符號引用,將目標文件中的占位符替換為符號的地址。鏈接器還要完成程序中各目標文件的地址空間的組織,這可能涉及重定位工作。在C./C++程序的鏈接過程中主要涉及以下角色:
·靜態(tài)庫:指編譯鏈接時,把庫文件的代碼全部加入到可執(zhí)行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了,其**名一般為“.a”。
·動態(tài)庫:在編譯鏈接時并沒有把庫文件的代碼加入到可執(zhí)行文件中,而是在程序執(zhí)行時由運行時鏈接文件加載庫,這樣可執(zhí)行文件比較小,動態(tài)庫一般**名為“.so”。
·可執(zhí)行文件:將所有的二進制文件鏈接起來融合成一個可執(zhí)行程序,不管這些文件是目標二進制文件還是庫二進制文件。
C++編譯特性
編譯單元
C/C++的編譯系統(tǒng)和其他高級語言存在很大的差異,其他高級語言中,編譯單元是整個Module,即Module下所有源碼,會在同一個編譯任務中執(zhí)行。而在C/C++中,編譯單元是以文件為單位。每個.c/.cc/.cxx/.cpp源文件是一個**的編譯單元,導致編譯優(yōu)化時只能基于本文件內容進行優(yōu)化,很難跨編譯單元提供代碼優(yōu)化。
頭文件解析
如果N個源文件引用到了同一個頭文件,則這個頭文件需要解析N次。
如果頭文件中有模板(STL/Boost),則該模板在每個cpp文件中使用時都會做一次實例化,N個源文件中的std::vector會實例化N次。
模板函數(shù)實例化
在C++ 98語言標準中,對于源代碼**現(xiàn)的每一處模板實例化,編譯器都需要去做實例化的工作;而在鏈接時,鏈接器還需要移除重復的實例化代碼。顯然編譯器遇到一個模板定義時,每次都去進行重復的實例化工作,進行重復的編譯工作。此時,如果能夠讓編譯器避免此類重復的實例化工作,那么可以**提高編譯器的工作效率。在C++ 0x標準中一個新的語言特性–外部模板的引入解決了這個問題。
在C++ 98中,已經有一個叫做顯式實例化(Explicit Instantiation)的語言特性,它的目的是指示編譯器立即進行模板實例化操作(即強制實例化)。而外部模板語法就是在顯式實例化指令的語法基礎上進行修改得到的,通過在顯式實例化指令前添加前綴extern,從而得到外部模板的語法。
.顯式實例化語法:template class vector。
.外部模板語法:extern template class vector。
一旦在一個編譯單元中使用了外部模板聲明,那么編譯器在編譯該編譯單元時,會跳過與該外部模板聲明匹配的模板實例化。