溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

發(fā)布時(shí)間:2021-10-21 16:56:47 來源:億速云 閱讀:125 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么”吧!

一、背景

大型C++工程項(xiàng)目,都會(huì)面臨編譯耗時(shí)較長的問題。不管是開發(fā)調(diào)試迭代、準(zhǔn)入測(cè)試,亦或是持續(xù)集成階段,編譯行為無處不在,降低編譯時(shí)間對(duì)提高研發(fā)效率來說具有非常重要意義。

美團(tuán)搜索與NLP部為公司提供基礎(chǔ)的搜索平臺(tái)服務(wù),出于性能的考慮,底層的基礎(chǔ)服務(wù)通過C++語言實(shí)現(xiàn),其中我們負(fù)責(zé)的深度查詢理解服務(wù)(DeepQueryUnderstanding,下文簡稱DQU)也面臨著編譯耗時(shí)較長這個(gè)問題,整個(gè)服務(wù)代碼在優(yōu)化前編譯時(shí)間需要二十分鐘左右(32核機(jī)器并行編譯),已經(jīng)影響到了團(tuán)隊(duì)開發(fā)迭代的效率?;谶@樣的背景,我們針對(duì)DQU服務(wù)的編譯問題進(jìn)行了專項(xiàng)優(yōu)化。在這個(gè)過程中,我們也積累了一些優(yōu)化的知識(shí)和經(jīng)驗(yàn),在這里分享給大家。

二、編譯原理及分析

2.1 編譯原理介紹

為了更好地理解編譯優(yōu)化方案,在介紹優(yōu)化方案之前,我們先簡單介紹一下編譯原理,通常我們?cè)谶M(jìn)行C++開發(fā)時(shí),編譯的過程主要包含下面四個(gè)步驟:

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

預(yù)處理器:宏定義替換,頭文件展開,條件編譯展開,刪除注釋。

  • gcc -E選項(xiàng)可以得到預(yù)處理后的結(jié)果,擴(kuò)展名為.i 或 .ii。

  • C/C++預(yù)處理不做任何語法檢查,不僅是因?yàn)樗痪邆湔Z法檢查功能,也因?yàn)轭A(yù)處理命令不屬于C/C++語句(這也是定義宏時(shí)不要加分號(hào)的原因),語法檢查是編譯器要做的事情。

  • 預(yù)處理之后,得到的僅僅是真正的源代碼。

編譯器:生成匯編代碼,得到匯編語言程序(把高級(jí)語言翻譯為機(jī)器語言),該種語言程序中的每條語句都以一種標(biāo)準(zhǔn)的文本格式確切的描述了一條低級(jí)機(jī)器語言指令。

  • gcc -S選項(xiàng)可以得到編譯后的匯編代碼文件,擴(kuò)展名為.s。

  • 匯編語言為不同高級(jí)語言的不同編譯器提供了通用的輸出語言。

匯編器:生成目標(biāo)文件。

  • gcc -c選項(xiàng)可以得到匯編后的結(jié)果文件,擴(kuò)展名為.o。

  • .o文件,是按照的二進(jìn)制編碼方式生成的文件。

鏈接器:生成可執(zhí)行文件或庫文件。

  • 靜態(tài)庫:指編譯鏈接時(shí),把庫文件的代碼全部加入到可執(zhí)行文件中,因此生成的文件比較大,但在運(yùn)行時(shí)也就不再需要庫文件了,其后綴名一般為“.a”。

  • 動(dòng)態(tài)庫:在編譯鏈接時(shí)并沒有把庫文件的代碼加入到可執(zhí)行文件中,而是在程序執(zhí)行時(shí)由運(yùn)行時(shí)鏈接文件加載庫,這樣可執(zhí)行文件比較小,動(dòng)態(tài)庫一般后綴名為“.so”。

  • 可執(zhí)行文件:將所有的二進(jìn)制文件鏈接起來融合成一個(gè)可執(zhí)行程序,不管這些文件是目標(biāo)二進(jìn)制文件還是庫二進(jìn)制文件。

2.2 C++編譯特點(diǎn)

(1)每個(gè)源文件獨(dú)立編譯

C/C++的編譯系統(tǒng)和其他高級(jí)語言存在很大的差異,其他高級(jí)語言中,編譯單元是整個(gè)Module,即Module下所有源碼,會(huì)在同一個(gè)編譯任務(wù)中執(zhí)行。而在C/C++中,編譯單元是以文件為單位。每個(gè).c/.cc/.cxx/.cpp源文件是一個(gè)獨(dú)立的編譯單元,導(dǎo)致編譯優(yōu)化時(shí)只能基于本文件內(nèi)容進(jìn)行優(yōu)化,很難跨編譯單元提供代碼優(yōu)化。

(2)每個(gè)編譯單元,都需要獨(dú)立解析所有包含的頭文件

如果N個(gè)源文件引用到了同一個(gè)頭文件,則這個(gè)頭文件需要解析N次(對(duì)于Thrift文件或者Boost頭文件這類動(dòng)輒幾千上萬行的頭文件來說,簡直就是“鬼故事”)。

如果頭文件中有模板(STL/Boost),則該模板在每個(gè)cpp文件中使用時(shí)都會(huì)做一次實(shí)例化,N個(gè)源文件中的std::vector<int>會(huì)實(shí)例化N次。

(3)模板函數(shù)實(shí)例化

在C++ 98語言標(biāo)準(zhǔn)中,對(duì)于源代碼中出現(xiàn)的每一處模板實(shí)例化,編譯器都需要去做實(shí)例化的工作;而在鏈接時(shí),鏈接器還需要移除重復(fù)的實(shí)例化代碼。顯然編譯器遇到一個(gè)模板定義時(shí),每次都去進(jìn)行重復(fù)的實(shí)例化工作,進(jìn)行重復(fù)的編譯工作。此時(shí),如果能夠讓編譯器避免此類重復(fù)的實(shí)例化工作,那么可以大大提高編譯器的工作效率。在C++ 0x標(biāo)準(zhǔn)中一個(gè)新的語言特性 -- 外部模板的引入解決了這個(gè)問題。

在C++ 98中,已經(jīng)有一個(gè)叫做顯式實(shí)例化(Explicit Instantiation)的語言特性,它的目的是指示編譯器立即進(jìn)行模板實(shí)例化操作(即強(qiáng)制實(shí)例化)。而外部模板語法就是在顯式實(shí)例化指令的語法基礎(chǔ)上進(jìn)行修改得到的,通過在顯式實(shí)例化指令前添加前綴extern,從而得到外部模板的語法。

① 顯式實(shí)例化語法:template class vector<MyClass>。 ② 外部模板語法:extern template class vector<MyClass>。

一旦在一個(gè)編譯單元中使用了外部模板聲明,那么編譯器在編譯該編譯單元時(shí),會(huì)跳過與該外部模板聲明匹配的模板實(shí)例化。

(4)虛函數(shù)

編譯器處理虛函數(shù)的方法是:給每個(gè)對(duì)象添加一個(gè)指針,存放了指向虛函數(shù)表的地址,虛函數(shù)表存儲(chǔ)了該類(包括繼承自基類)的虛函數(shù)地址。如果派生類重寫了虛函數(shù)的新定義,該虛函數(shù)表將保存新函數(shù)的地址,如果派生類沒有重新定義虛函數(shù),該虛函數(shù)表將保存函數(shù)原始版本的地址。如果派生類定義了新的虛函數(shù),則該函數(shù)的地址將被添加到虛函數(shù)表中。

調(diào)用虛函數(shù)時(shí),程序?qū)⒉榭创鎯?chǔ)在對(duì)象中的虛函數(shù)表地址,轉(zhuǎn)向相應(yīng)的虛函數(shù)表,使用類聲明中定義的第幾個(gè)虛函數(shù),程序就使用數(shù)組的第幾個(gè)函數(shù)地址,并執(zhí)行該函數(shù)。

使用虛函數(shù)后的變化:

① 對(duì)象將增加一個(gè)存儲(chǔ)地址的空間(32位系統(tǒng)為4字節(jié),64位為8字節(jié))。 ② 每個(gè)類編譯器都創(chuàng)建一個(gè)虛函數(shù)地址表。 ③ 對(duì)每個(gè)函數(shù)調(diào)用都需要增加在表中查找地址的操作。

(5)編譯優(yōu)化

GCC提供了為了滿足用戶不同程度的的優(yōu)化需要,提供了近百種優(yōu)化選項(xiàng),用來對(duì)編譯時(shí)間,目標(biāo)文件長度,執(zhí)行效率這個(gè)三維模型進(jìn)行不同的取舍和平衡。優(yōu)化的方法不一而足,總體上將有以下幾類:

① 精簡操作指令。 ② 盡量滿足CPU的流水操作。 ③ 通過對(duì)程序行為地猜測(cè),重新調(diào)整代碼的執(zhí)行順序。 ④ 充分使用寄存器。 ⑤ 對(duì)簡單的調(diào)用進(jìn)行展開等等。

如果全部了解這些編譯選項(xiàng),對(duì)代碼針對(duì)性的優(yōu)化還是一項(xiàng)復(fù)雜的工作,幸運(yùn)的是GCC提供了從O0-O3以及Os這幾種不同的優(yōu)化級(jí)別供大家選擇,在這些選項(xiàng)中,包含了大部分有效的編譯優(yōu)化選項(xiàng),并且可以在這個(gè)基礎(chǔ)上,對(duì)某些選項(xiàng)進(jìn)行屏蔽或添加,從而大大降低了使用的難度。

  • O0:不做任何優(yōu)化,這是默認(rèn)的編譯選項(xiàng)。

  • O和O1:對(duì)程序做部分編譯優(yōu)化,編譯器會(huì)嘗試減小生成代碼的尺寸,以及縮短執(zhí)行時(shí)間,但并不執(zhí)行需要占用大量編譯時(shí)間的優(yōu)化。

  • O2:是比O1更高級(jí)的選項(xiàng),進(jìn)行更多的優(yōu)化。GCC將執(zhí)行幾乎所有的不包含時(shí)間和空間折中的優(yōu)化。當(dāng)設(shè)置O2選項(xiàng)時(shí),編譯器并不進(jìn)行循環(huán)展開以及函數(shù)內(nèi)聯(lián)優(yōu)化。與O1比較而言,O2優(yōu)化增加了編譯時(shí)間的基礎(chǔ)上,提高了生成代碼的執(zhí)行效率。

  • O3:在O2的基礎(chǔ)上進(jìn)行更多的優(yōu)化,例如使用偽寄存器網(wǎng)絡(luò),普通函數(shù)的內(nèi)聯(lián),以及針對(duì)循環(huán)的更多優(yōu)化。

  • Os:主要是對(duì)代碼大小的優(yōu)化, 通常各種優(yōu)化都會(huì)打亂程序的結(jié)構(gòu),讓調(diào)試工作變得無從著手。并且會(huì)打亂執(zhí)行順序,依賴內(nèi)存操作順序的程序需要做相關(guān)處理才能確保程序的正確性。

編譯優(yōu)化有可能帶來的問題:

調(diào)試問題:正如上面所提到的,任何級(jí)別的優(yōu)化都將帶來代碼結(jié)構(gòu)的改變。例如:對(duì)分支的合并和消除,對(duì)公用子表達(dá)式的消除,對(duì)循環(huán)內(nèi)load/store操作的替換和更改等,都將會(huì)使目標(biāo)代碼的執(zhí)行順序變得面目全非,導(dǎo)致調(diào)試信息嚴(yán)重不足。

內(nèi)存操作順序改變問題:在O2優(yōu)化后,編譯器會(huì)對(duì)影響內(nèi)存操作的執(zhí)行順序。例如:-fschedule-insns允許數(shù)據(jù)處理時(shí)先完成其他的指令;-fforce-mem有可能導(dǎo)致內(nèi)存與寄存器之間的數(shù)據(jù)產(chǎn)生類似臟數(shù)據(jù)的不一致等。對(duì)于某些依賴內(nèi)存操作順序而進(jìn)行的邏輯,需要做嚴(yán)格的處理后才能進(jìn)行優(yōu)化。例如,采用Volatile關(guān)鍵字限制變量的操作方式,或者利用Barrier迫使CPU嚴(yán)格按照指令序執(zhí)行。

(6)C/C++ 跨編譯單元的優(yōu)化只能交給鏈接器

當(dāng)鏈接器進(jìn)行鏈接的時(shí)候,首先決定各個(gè)目標(biāo)文件在最終可執(zhí)行文件里的位置。然后訪問所有目標(biāo)文件的地址重定義表,對(duì)其中記錄的地址進(jìn)行重定向(加上一個(gè)偏移量,即該編譯單元在可執(zhí)行文件上的起始地址)。然后遍歷所有目標(biāo)文件的未解決符號(hào)表,并且在所有的導(dǎo)出符號(hào)表里查找匹配的符號(hào),并在未解決符號(hào)表中所記錄的位置上填寫實(shí)現(xiàn)地址,最后把所有的目標(biāo)文件的內(nèi)容寫在各自的位置上,就生成一個(gè)可執(zhí)行文件。鏈接的細(xì)節(jié)比較復(fù)雜,鏈接階段是單進(jìn)程,無法并行加速,導(dǎo)致大項(xiàng)目鏈接極慢。

三、服務(wù)問題分析

DQU是美團(tuán)搜索使用的查詢理解平臺(tái),內(nèi)部包含了大量的模型、詞表、在代碼結(jié)構(gòu)上,包含20多個(gè)Thrift文件 ,使用大量Boost處理函數(shù) ,同時(shí)引入了SF框架,公司第三方組件SDK以及分詞三個(gè)Submodule,各個(gè)模塊采用動(dòng)態(tài)庫編譯加載的方式,模塊之間通過消息總線做數(shù)據(jù)的傳輸,消息總線是一個(gè)大的Event類,這樣這個(gè)類就包含了各個(gè)模塊需要的數(shù)據(jù)類型的定義,所以各個(gè)模塊都會(huì)引入Event頭文件,不合理的依賴關(guān)系造成這個(gè)文件被改動(dòng),幾乎所有的模塊都會(huì)重新編譯。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

每個(gè)服務(wù)所面臨的編譯問題都有各自的特點(diǎn),但是遇到問題的本質(zhì)原因是類似的,結(jié)合編譯的過程和原理,我們從預(yù)編譯展開、頭文件依賴以及編譯過程耗時(shí)3個(gè)方面對(duì)DQU服務(wù)編譯問題進(jìn)行了分析。

3.1 編譯展開分析

編譯展開分析就是通過C++的預(yù)編譯階段保留的.ii文件,查看通過展開后的編譯文件大小,具體可以通過在cmake中指定編譯選型 “-save-temps” 保留編譯中間文件。

set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS} -ggdb -Og -fPIC -w -Wl,--export-dynamic -Wno-deprecated -fpermissive -save-temps")

編譯耗時(shí)的最直接原因就是編譯文件展開之后比較大,通過編譯展開后的文件大小和內(nèi)容,通過預(yù)編譯展開分析能看到文件展開后的文件有40多萬行,發(fā)現(xiàn)有大量的Boost庫引用及頭文件引用造成的展開文件比較大,影響到編譯的耗時(shí)。通過這個(gè)方式能夠找到各個(gè)文件編譯耗時(shí)的共性,下圖是編譯展開后文件大小截圖。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

3.2 頭文件依賴分析

頭文件依賴分析是從引用頭文件數(shù)量的角度來看代碼是否合理的一種分析方式,我們實(shí)現(xiàn)了一個(gè)腳本,用來統(tǒng)計(jì)頭文件的依賴關(guān)系,并且分析輸出頭文件依賴引用計(jì)數(shù),用來輔助判斷頭文件依賴關(guān)系是否合理。

(1) 頭文件引用總數(shù)結(jié)果統(tǒng)計(jì)

通過工具統(tǒng)計(jì)出編譯源文件直接和間接依賴的頭文件的總個(gè)數(shù),用來從頭文件引入數(shù)量上分析問題。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

(2) 單個(gè)頭文件依賴關(guān)系統(tǒng)計(jì)

通過工具分析頭文件依賴關(guān)系,生成依賴關(guān)系拓?fù)鋱D,能夠直觀的看到依賴不合理的地方。

圖中包含引用層次關(guān)系,以及引用頭文件個(gè)數(shù)。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

3.3 編譯耗時(shí)結(jié)果分段統(tǒng)計(jì)

編譯耗時(shí)分段統(tǒng)計(jì)是從結(jié)果上看各個(gè)文件的編譯耗時(shí)以及各個(gè)編譯階段的耗時(shí)情況,這個(gè)是直觀的一個(gè)結(jié)果,正常情況下,是和文件展開大小以及頭文件引用個(gè)數(shù)是正相關(guān)的,cmake通過指定環(huán)境變量能打印出編譯和鏈接階段的耗時(shí)情況,通過這個(gè)數(shù)據(jù)能直觀的分析出耗時(shí)情況。

set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time")
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CMAKE_COMMAND} -E time")

編譯耗時(shí)結(jié)果輸出:

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

3.4 分析工具建設(shè)

通過上面的工具分析能拿到幾個(gè)編譯數(shù)據(jù):

① 頭文件依賴關(guān)系及個(gè)數(shù)。 ② 預(yù)編譯展開大小及內(nèi)容。 ③ 各個(gè)文件編譯耗時(shí)。 ④ 整體鏈接耗時(shí)。 ⑤ 可以計(jì)算出編譯并行度。

通過這幾個(gè)數(shù)據(jù)的輸入我們考慮可以做個(gè)自動(dòng)化分析工具,找出優(yōu)化點(diǎn)以及界面化展示?;谶@個(gè)目的,我們建設(shè)了全流程自動(dòng)化分析工具,能夠自動(dòng)分析耗時(shí)共性問題以及TopN耗時(shí)文件。分析工具處理流程如下圖所示:

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

(1) 整體統(tǒng)計(jì)分析效果

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

具體字段說明:

① cost_time 編譯耗時(shí),單位是秒。 ② file_compile_size,編譯中間文件大小,單位是M。 ③ file_name,文件名稱。 ④ include_h_nums,引入頭文件個(gè)數(shù),單位是個(gè)。 ⑤ top_h_files_info, 引入最多的TopN頭文件。

(2)Top10 編譯耗時(shí)文件統(tǒng)計(jì)

用來展示統(tǒng)計(jì)編譯耗時(shí)最久的TopN文件,N可以自定義指定。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

(3)Top10編譯中間文件大小統(tǒng)計(jì)

通過統(tǒng)計(jì)和展示編譯文件大小,用來判斷這塊是否符合預(yù)期,這個(gè)是和編譯耗時(shí)一一對(duì)應(yīng)的。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

(4)Top10引入最多頭文件的頭文件統(tǒng)計(jì)

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

(5)Top10頭文件重復(fù)次數(shù)統(tǒng)計(jì)

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

目前,這個(gè)工具支持一鍵化生成編譯耗時(shí)分析結(jié)果,其中幾個(gè)小工具,比如依賴文件個(gè)數(shù)工具已經(jīng)集成到公司的上線集成測(cè)試流程中,通過自動(dòng)化工具檢查代碼改動(dòng)對(duì)編譯耗時(shí)的影響,工具的建設(shè)還在不斷迭代優(yōu)化中,后續(xù)會(huì)集成到公司的MCD平臺(tái)中,可以自動(dòng)分析來定位編譯耗時(shí)長的問題,解決其它部門編譯耗時(shí)問題。

四、優(yōu)化方案與實(shí)踐

通過運(yùn)用上述相關(guān)工具,我們能夠發(fā)現(xiàn)Top10編譯耗時(shí)文件的共性,比如都依賴消息總線文件platform_query_analysis_enent.h,這個(gè)文件又直接間接引入2000多個(gè)頭文件,我們重點(diǎn)優(yōu)化了這類文件,通過工具的編譯展開,找出了Boost使用、模板類展開、Thrift頭文件展開等共性問題,并針對(duì)這些問題做專門的優(yōu)化。此外,我們也使用了一些業(yè)內(nèi)通用的編譯優(yōu)化方案,并取得了不錯(cuò)的效果。下面詳細(xì)介紹我們采用的各種優(yōu)化方案。

4.1 通用編譯加速方案

業(yè)內(nèi)有不少通用編譯加速工具(方案),無需侵入代碼就能提高編譯速度,非常值得嘗試。

(1)并行編譯

在Linux平臺(tái)上一般使用GNU的Make工具進(jìn)行編譯,在執(zhí)行make命令時(shí)可以加上-j參數(shù)增加編譯并行度,如make -j 4將開啟4個(gè)任務(wù)。在實(shí)踐中我們并不將該參數(shù)寫死,而是通過$(nproc)方法動(dòng)態(tài)獲取編譯機(jī)的CPU核數(shù)作為編譯并發(fā)度,從而最大限度利用多核的性能優(yōu)勢(shì)。

(2)分布式編譯

使用分布式編譯技術(shù),比如利用Distcc和Dmucs構(gòu)建大規(guī)模、分布式C++編譯環(huán)境,Linux平臺(tái)利用網(wǎng)絡(luò)集群進(jìn)行分布式編譯,需要考慮網(wǎng)絡(luò)時(shí)延與網(wǎng)絡(luò)穩(wěn)定性。分布式編譯適合規(guī)模較大的項(xiàng)目,比如單機(jī)編譯需要數(shù)小時(shí)甚至數(shù)天。DQU服務(wù)從代碼規(guī)模以及單機(jī)編譯時(shí)長來說,暫時(shí)還不需要使用分布式的方式來加速,具體細(xì)節(jié)可以參考Distcc官方文檔說明。

(3)預(yù)編譯頭文件

PCH(Precompiled Header),該方法預(yù)先將常用頭文件的編譯結(jié)果保存起來,這樣編譯器在處理對(duì)應(yīng)的頭文件引入時(shí)可以直接使用預(yù)先編譯好的結(jié)果,從而加快整個(gè)編譯流程。PCH是業(yè)內(nèi)十分常用的加速編譯的方法,且大家反饋效果非常不錯(cuò)。在我們的項(xiàng)目中,由于涉及到很多Shared Library的編譯生成,而Shared Library相互之間無法共享PCH,因此沒有取得預(yù)想效果。

(4)CCache

CCache(Compiler Cache是一個(gè)編譯緩存工具,其原理是將cpp的編譯結(jié)果保存在文件緩存中,以后編譯時(shí)若對(duì)應(yīng)文件無變動(dòng)可直接從緩存中獲取編譯結(jié)果。需要注意的是,Make本身也有一定緩存功能,當(dāng)目標(biāo)文件已編譯(且依賴無變化)時(shí),若源文件時(shí)間戳無變化也不會(huì)再次編譯;但CCache是按文件內(nèi)容做的緩存,且同一機(jī)器的多個(gè)項(xiàng)目可以共享緩存,因此適用面更大。

(5)Module編譯

如果你的項(xiàng)目是用C++ 20進(jìn)行開發(fā)的,那么恭喜你,Module編譯也是一個(gè)優(yōu)化編譯速度的方案,C++20之前的版本會(huì)把每一個(gè)cpp當(dāng)做一個(gè)編譯單元處理,會(huì)存在引入的頭文件被多次解析編譯的問題。而Module的出現(xiàn)就是解決這一問題,Module不再需要頭文件(只需要一個(gè)模塊文件,不需要聲明和實(shí)現(xiàn)兩個(gè)文件),它會(huì)將你的(.ixx 或者 .cppm)模塊實(shí)體直接編譯,并自動(dòng)生成一個(gè)二進(jìn)制接口文件。import和include預(yù)處理不同,編譯好的模塊下次import的時(shí)候不會(huì)重復(fù)編譯,可以大幅度提高編譯器的效率。

(6)自動(dòng)依賴分析

Google也推出了開源的Include-What-You-Use工具(簡稱IWYU),基于Clang的C/C++工程冗余頭文件檢查工具。IWYU依賴Clang編譯套件,使用該工具可以掃描出文件依賴問題,同時(shí)該工具還提供腳本解決頭文件依賴問題,我們嘗試搭建了這套分析工具,這個(gè)工具也提供自動(dòng)化頭文件解決方案,但是由于我們的代碼依賴比較復(fù)雜,有動(dòng)態(tài)庫、靜態(tài)庫、子倉庫等,這個(gè)工具提供的優(yōu)化功能不能直接使用,其它團(tuán)隊(duì)如果代碼結(jié)構(gòu)比較簡單的話,可以考慮使用這個(gè)工具分析優(yōu)化,會(huì)生成如下結(jié)果文件,指導(dǎo)哪些頭文件需要?jiǎng)h除。

>>> Fixing #includes in '/opt/meituan/zhoulei/query_analysis/src/common/qa/record/brand_record.h'
@@ -1,9 +1,10 @@

 #ifndef _MTINTENTION_DATA_BRAND_RECORD_H_
 #define _MTINTENTION_DATA_BRAND_RECORD_H_
-#include "qa/data/record.h"
-#include "qa/data/template_map.hpp"
-#include "qa/data/template_vector.hpp"
-#include <boost/serialization/version.hpp>
+#include <boost/serialization/version.hpp>  // for BOOST_CLASS_VERSION
+#include <string>                       // for string
+#include <vector>                       // for vector
+
+#include "qa/data/file_buffer.h"        // for REG_TEMPLATE_FILE_HANDLER

4.2 代碼優(yōu)化方案與實(shí)踐

(1)前置類型聲明

通過分析頭文件引用統(tǒng)計(jì),我們發(fā)現(xiàn)項(xiàng)目中被引用最多的是總線類型Event,而該類型中又放置了各種業(yè)務(wù)需要的成員,示例如下:

#include “a.h”
#include "b.h"
class Event {
// 業(yè)務(wù)A, B, C ...
  A1 a1;
  A2 a2;
 	// ...
  B1 b1;
  B2 b2;
  // ...
};

這導(dǎo)致Event中包含了數(shù)量龐大的頭文件,在頭文件展開后,文件大小達(dá)到15M;而各種業(yè)務(wù)都會(huì)需要使用Event,自然會(huì)嚴(yán)重拖累編譯性能。

我們通過前置類型聲明來解決這個(gè)問題,即不引入對(duì)應(yīng)類型的頭文件,只做前置聲明,在Event中只使用對(duì)應(yīng)類型的指針,如下所示:

class A2;
// ...
class Event {
// 業(yè)務(wù)A, B, C ...
  shared_ptr<A1> a1;
  shared_ptr<A2> a2;
  // ...
  shared_ptr<B1> b1;
  shared_ptr<B2> b2;
  // ...
};

只有在真正使用對(duì)應(yīng)成員變量時(shí),才需要引入對(duì)應(yīng)頭文件;這樣真正做到了按需引入頭文件。

(2)外部模板

由于模板被使用時(shí)才會(huì)實(shí)例化這一特性,相同的實(shí)例可以出現(xiàn)在多個(gè)文件對(duì)象中。編譯器要對(duì)每一處模板進(jìn)行實(shí)例化,鏈接器還要移除重復(fù)的實(shí)例化代碼。當(dāng)在廣泛使用模板的項(xiàng)目中,編譯器會(huì)產(chǎn)生大量的冗余代碼,這會(huì)極大地增加編譯時(shí)間和鏈接時(shí)間。C++ 11新標(biāo)準(zhǔn)中可以通過外部模板來避免。

// util.h
template <typename T> 
void max(T) { ... }
// A.cpp
extern template void max<int>(int);
#include "util.h"
template void max<int>(int); // 顯式地實(shí)例化 
void test1()
{ 
    max(1);
}

在編譯A.cpp的時(shí)候,實(shí)例化出一個(gè) max<int>(int)版本的函數(shù)。

// B.cpp
#include "util.h"
extern template void max<int>(int); // 外部模板的聲明
void test2()
{
    max(2);
}

在編譯B.cpp的時(shí)候,就不再生成 max<int>(int)實(shí)例化代碼,這樣就節(jié)省了前面提到的實(shí)例化,編譯以及鏈接的耗時(shí)了。

(3)多態(tài)替換模板使用

我們的項(xiàng)目重度使用詞典相關(guān)操作,如加載詞典、解析詞典、匹配詞典(各種花式匹配),這些操作都是通過Template模板擴(kuò)展支持各種不同類型的詞典。據(jù)統(tǒng)計(jì),詞典的類型超過150個(gè),這也造成模板展開的代碼量膨脹。

template <class R>
class Dict {
public:
  // 匹配key和condition,賦值給record
  bool match(const string &key, const string &condition, R &record);  // 對(duì)每種類型的Record都會(huì)展開一次
private:
  map<string, R> dict;
};

幸運(yùn)的是,我們?cè)~典的絕大部分操作都可以抽象出幾類接口,因此可以只實(shí)現(xiàn)針對(duì)基類的操作:

class Record {  // 基類
public:
  virtual bool match(const string &condition);  // 派生類需實(shí)現(xiàn)
};

class Dict {
public:
  shared_ptr<Record> match(const string &key, const string &condition);  // 使用方傳入派生類的指針即可
private:
  map<string, shared_ptr<Record>> dict;
};

通過繼承和多態(tài),我們有效避免了大量的模板展開。需要注意的是,使用指針作為Map的Value會(huì)增加內(nèi)存分配的壓力,推薦使用Tcmalloc或Jemalloc替換默認(rèn)的Ptmalloc優(yōu)化內(nèi)存分配。

(4)替換Boost庫

Boost是一個(gè)廣泛使用的基礎(chǔ)庫,涵蓋了大量常用函數(shù),十分方便、好用,然而也存在一些不足之處。一個(gè)顯著缺點(diǎn)是其實(shí)現(xiàn)采用了hpp的形式,即聲明和實(shí)現(xiàn)均放在頭文件中,這會(huì)造成預(yù)編譯展開后十分巨大。

// 字符串操作是常用功能,僅僅引入該頭文件展開大小就超過4M
#include <boost/algorithm/string.hpp>
// 與此相對(duì)的,引入多個(gè)STL的頭文件,展開后僅僅只有1M
#include <vector>
#include <map>
// ...

在我們項(xiàng)目中主要使用的Boost函數(shù)不超過二十個(gè),部分可以在STL中找到替代,部分我們手動(dòng)做了實(shí)現(xiàn),使得項(xiàng)目從重度依賴Boost轉(zhuǎn)變成絕大部分達(dá)到Boost-Free,大大降低了編譯的負(fù)擔(dān)。

(5)預(yù)編譯

代碼中有一些平常改動(dòng)比較少,但是對(duì)編譯耗時(shí)產(chǎn)生一定的影響,比如Thrift生成的文件,模型庫文件以及Common目錄下的通用文件,我們采取提起預(yù)編譯成動(dòng)態(tài)庫,減少后續(xù)文件的編譯耗時(shí),也解決了部分編譯依賴。

(6)解決編譯依賴,提高編譯并行度

在我們項(xiàng)目中有大量模塊級(jí)別的動(dòng)態(tài)庫文件需要編譯,cmake文件指定的編譯依賴關(guān)系在一定程度上限制了編譯并行度的執(zhí)行。

比如下面這個(gè)場(chǎng)景,通過合理設(shè)置庫文件依賴關(guān)系,可以提高編譯并行度。

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

4.3 優(yōu)化效果

我們通過32C、64G內(nèi)存機(jī)器做了編譯耗時(shí)優(yōu)化前后的效果對(duì)比,統(tǒng)計(jì)結(jié)果如下:

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么

4.4 守住優(yōu)化成果

編譯優(yōu)化是一件“逆水行舟”的事情,開發(fā)人員總是傾向于不斷增加新的功能、新的庫乃至新的框架,而要?jiǎng)h除舊代碼、舊庫、下線舊框架總是困難重重(相信一線開發(fā)人員一定深有體會(huì))。因此,如何守住之前取得的優(yōu)化成果也是至關(guān)重要的。我們?cè)趯?shí)踐中有以下幾點(diǎn)體會(huì):

  • 代碼審核是困難的(引起編譯耗時(shí)增加的改動(dòng),往往無法通過審核代碼直觀地發(fā)現(xiàn))。

  • 工具、流程才值得依賴。

  • 關(guān)鍵在于控制增量。

我們發(fā)現(xiàn),cpp文件的編譯耗時(shí),和其預(yù)編譯展開文件(.ii)大小呈正相關(guān)(絕大部分情況下);對(duì)每一個(gè)上線版本,將其所有cpp文件的預(yù)編譯展開大小記錄下來,就形成了其編譯指紋(CF,Compile Fingerprint)。通過比較相鄰兩個(gè)版本的CF,就能較準(zhǔn)確的知道新版帶來的編譯耗時(shí)主要由哪些改動(dòng)引入,并可以進(jìn)一步分析耗時(shí)上漲是否合理,是否有優(yōu)化空間。

我們將該種方式制作成腳本工具并引入上線流程,從而能夠很清楚的了解每次代碼發(fā)版帶來的編譯性能影響,并有效地幫助我們守住前期的優(yōu)化成果。

五、總結(jié)

DQU項(xiàng)目是美團(tuán)搜索業(yè)務(wù)環(huán)節(jié)中重要的一環(huán),該系統(tǒng)需要對(duì)接20+RPC、數(shù)十個(gè)模型、加載超過300個(gè)詞典,使用內(nèi)存數(shù)十G,日均響應(yīng)請(qǐng)求超過20億的大型C++服務(wù)。在業(yè)務(wù)高速迭代的情況,冗長的編譯時(shí)間為開發(fā)同學(xué)帶來較大的困擾,一定程度上制約了開發(fā)效率。最終我們通過編譯優(yōu)化分析工具建設(shè),結(jié)合采用了通用編譯優(yōu)化加速方案和代碼層面的優(yōu)化,將DQU的編譯時(shí)間縮短了70%,并通過引CCache等手段,使得本地開發(fā)的編譯,能夠在100s內(nèi)完成,給開發(fā)團(tuán)隊(duì)節(jié)省了大量的時(shí)間。

在取得階段性成果之后,我們總結(jié)整個(gè)問題解決的過程,并沉淀出一些分析方法、工具以及流程規(guī)范。這些工具在后續(xù)的開發(fā)迭代過程中,能夠快速有效地檢測(cè)新的代碼變更帶來的編譯時(shí)間變化,并成為了我們的上線流程檢查中的一環(huán)檢測(cè)標(biāo)準(zhǔn)。這一點(diǎn)與我們以往一次性的或者針對(duì)性的編譯優(yōu)化,產(chǎn)生了很大的區(qū)別。畢竟代碼的維護(hù)是一個(gè)持久的過程,系統(tǒng)化的解決這一問題,不只是需要有效的方法和便捷的工具,更需要一個(gè)標(biāo)準(zhǔn)化的,規(guī)范化的上線流程來保持成果。

到此,相信大家對(duì)“C++服務(wù)編譯耗時(shí)優(yōu)化原理是什么”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

c++
AI