本篇內容主要講解“C++服務編譯耗時優(yōu)化原理是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“C++服務編譯耗時優(yōu)化原理是什么”吧!
站在用戶的角度思考問題,與客戶深入溝通,找到綏化網(wǎng)站設計與綏化網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗,讓設計與互聯(lián)網(wǎng)技術結合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:網(wǎng)站制作、網(wǎng)站設計、企業(yè)官網(wǎng)、英文網(wǎng)站、手機端網(wǎng)站、網(wǎng)站推廣、主機域名、網(wǎng)站空間、企業(yè)郵箱。業(yè)務覆蓋綏化地區(qū)。
大型C++工程項目,都會面臨編譯耗時較長的問題。不管是開發(fā)調試迭代、準入測試,亦或是持續(xù)集成階段,編譯行為無處不在,降低編譯時間對提高研發(fā)效率來說具有非常重要意義。
美團搜索與NLP部為公司提供基礎的搜索平臺服務,出于性能的考慮,底層的基礎服務通過C++語言實現(xiàn),其中我們負責的深度查詢理解服務(DeepQueryUnderstanding,下文簡稱DQU)也面臨著編譯耗時較長這個問題,整個服務代碼在優(yōu)化前編譯時間需要二十分鐘左右(32核機器并行編譯),已經(jīng)影響到了團隊開發(fā)迭代的效率?;谶@樣的背景,我們針對DQU服務的編譯問題進行了專項優(yōu)化。在這個過程中,我們也積累了一些優(yōu)化的知識和經(jīng)驗,在這里分享給大家。
為了更好地理解編譯優(yōu)化方案,在介紹優(yōu)化方案之前,我們先簡單介紹一下編譯原理,通常我們在進行C++開發(fā)時,編譯的過程主要包含下面四個步驟:
預處理器:宏定義替換,頭文件展開,條件編譯展開,刪除注釋。
gcc -E選項可以得到預處理后的結果,擴展名為.i 或 .ii。
C/C++預處理不做任何語法檢查,不僅是因為它不具備語法檢查功能,也因為預處理命令不屬于C/C++語句(這也是定義宏時不要加分號的原因),語法檢查是編譯器要做的事情。
預處理之后,得到的僅僅是真正的源代碼。
編譯器:生成匯編代碼,得到匯編語言程序(把高級語言翻譯為機器語言),該種語言程序中的每條語句都以一種標準的文本格式確切的描述了一條低級機器語言指令。
gcc -S選項可以得到編譯后的匯編代碼文件,擴展名為.s。
匯編語言為不同高級語言的不同編譯器提供了通用的輸出語言。
匯編器:生成目標文件。
gcc -c選項可以得到匯編后的結果文件,擴展名為.o。
.o文件,是按照的二進制編碼方式生成的文件。
鏈接器:生成可執(zhí)行文件或庫文件。
靜態(tài)庫:指編譯鏈接時,把庫文件的代碼全部加入到可執(zhí)行文件中,因此生成的文件比較大,但在運行時也就不再需要庫文件了,其后綴名一般為“.a”。
動態(tài)庫:在編譯鏈接時并沒有把庫文件的代碼加入到可執(zhí)行文件中,而是在程序執(zhí)行時由運行時鏈接文件加載庫,這樣可執(zhí)行文件比較小,動態(tài)庫一般后綴名為“.so”。
可執(zhí)行文件:將所有的二進制文件鏈接起來融合成一個可執(zhí)行程序,不管這些文件是目標二進制文件還是庫二進制文件。
(1)每個源文件獨立編譯
C/C++的編譯系統(tǒng)和其他高級語言存在很大的差異,其他高級語言中,編譯單元是整個Module,即Module下所有源碼,會在同一個編譯任務中執(zhí)行。而在C/C++中,編譯單元是以文件為單位。每個.c/.cc/.cxx/.cpp源文件是一個獨立的編譯單元,導致編譯優(yōu)化時只能基于本文件內容進行優(yōu)化,很難跨編譯單元提供代碼優(yōu)化。
(2)每個編譯單元,都需要獨立解析所有包含的頭文件
如果N個源文件引用到了同一個頭文件,則這個頭文件需要解析N次(對于Thrift文件或者Boost頭文件這類動輒幾千上萬行的頭文件來說,簡直就是“鬼故事”)。
如果頭文件中有模板(STL/Boost),則該模板在每個cpp文件中使用時都會做一次實例化,N個源文件中的std::vector
(3)模板函數(shù)實例化
在C++ 98語言標準中,對于源代碼中出現(xiàn)的每一處模板實例化,編譯器都需要去做實例化的工作;而在鏈接時,鏈接器還需要移除重復的實例化代碼。顯然編譯器遇到一個模板定義時,每次都去進行重復的實例化工作,進行重復的編譯工作。此時,如果能夠讓編譯器避免此類重復的實例化工作,那么可以大大提高編譯器的工作效率。在C++ 0x標準中一個新的語言特性 -- 外部模板的引入解決了這個問題。
在C++ 98中,已經(jīng)有一個叫做顯式實例化(Explicit Instantiation)的語言特性,它的目的是指示編譯器立即進行模板實例化操作(即強制實例化)。而外部模板語法就是在顯式實例化指令的語法基礎上進行修改得到的,通過在顯式實例化指令前添加前綴extern,從而得到外部模板的語法。
① 顯式實例化語法:template class vector
一旦在一個編譯單元中使用了外部模板聲明,那么編譯器在編譯該編譯單元時,會跳過與該外部模板聲明匹配的模板實例化。
(4)虛函數(shù)
編譯器處理虛函數(shù)的方法是:給每個對象添加一個指針,存放了指向虛函數(shù)表的地址,虛函數(shù)表存儲了該類(包括繼承自基類)的虛函數(shù)地址。如果派生類重寫了虛函數(shù)的新定義,該虛函數(shù)表將保存新函數(shù)的地址,如果派生類沒有重新定義虛函數(shù),該虛函數(shù)表將保存函數(shù)原始版本的地址。如果派生類定義了新的虛函數(shù),則該函數(shù)的地址將被添加到虛函數(shù)表中。
調用虛函數(shù)時,程序將查看存儲在對象中的虛函數(shù)表地址,轉向相應的虛函數(shù)表,使用類聲明中定義的第幾個虛函數(shù),程序就使用數(shù)組的第幾個函數(shù)地址,并執(zhí)行該函數(shù)。
使用虛函數(shù)后的變化:
① 對象將增加一個存儲地址的空間(32位系統(tǒng)為4字節(jié),64位為8字節(jié))。 ② 每個類編譯器都創(chuàng)建一個虛函數(shù)地址表。 ③ 對每個函數(shù)調用都需要增加在表中查找地址的操作。
(5)編譯優(yōu)化
GCC提供了為了滿足用戶不同程度的的優(yōu)化需要,提供了近百種優(yōu)化選項,用來對編譯時間,目標文件長度,執(zhí)行效率這個三維模型進行不同的取舍和平衡。優(yōu)化的方法不一而足,總體上將有以下幾類:
① 精簡操作指令。 ② 盡量滿足CPU的流水操作。 ③ 通過對程序行為地猜測,重新調整代碼的執(zhí)行順序。 ④ 充分使用寄存器。 ⑤ 對簡單的調用進行展開等等。
如果全部了解這些編譯選項,對代碼針對性的優(yōu)化還是一項復雜的工作,幸運的是GCC提供了從O0-O3以及Os這幾種不同的優(yōu)化級別供大家選擇,在這些選項中,包含了大部分有效的編譯優(yōu)化選項,并且可以在這個基礎上,對某些選項進行屏蔽或添加,從而大大降低了使用的難度。
O0:不做任何優(yōu)化,這是默認的編譯選項。
O和O1:對程序做部分編譯優(yōu)化,編譯器會嘗試減小生成代碼的尺寸,以及縮短執(zhí)行時間,但并不執(zhí)行需要占用大量編譯時間的優(yōu)化。
O2:是比O1更高級的選項,進行更多的優(yōu)化。GCC將執(zhí)行幾乎所有的不包含時間和空間折中的優(yōu)化。當設置O2選項時,編譯器并不進行循環(huán)展開以及函數(shù)內聯(lián)優(yōu)化。與O1比較而言,O2優(yōu)化增加了編譯時間的基礎上,提高了生成代碼的執(zhí)行效率。
O3:在O2的基礎上進行更多的優(yōu)化,例如使用偽寄存器網(wǎng)絡,普通函數(shù)的內聯(lián),以及針對循環(huán)的更多優(yōu)化。
Os:主要是對代碼大小的優(yōu)化, 通常各種優(yōu)化都會打亂程序的結構,讓調試工作變得無從著手。并且會打亂執(zhí)行順序,依賴內存操作順序的程序需要做相關處理才能確保程序的正確性。
編譯優(yōu)化有可能帶來的問題:
① 調試問題:正如上面所提到的,任何級別的優(yōu)化都將帶來代碼結構的改變。例如:對分支的合并和消除,對公用子表達式的消除,對循環(huán)內load/store操作的替換和更改等,都將會使目標代碼的執(zhí)行順序變得面目全非,導致調試信息嚴重不足。
② 內存操作順序改變問題:在O2優(yōu)化后,編譯器會對影響內存操作的執(zhí)行順序。例如:-fschedule-insns允許數(shù)據(jù)處理時先完成其他的指令;-fforce-mem有可能導致內存與寄存器之間的數(shù)據(jù)產(chǎn)生類似臟數(shù)據(jù)的不一致等。對于某些依賴內存操作順序而進行的邏輯,需要做嚴格的處理后才能進行優(yōu)化。例如,采用Volatile關鍵字限制變量的操作方式,或者利用Barrier迫使CPU嚴格按照指令序執(zhí)行。
(6)C/C++ 跨編譯單元的優(yōu)化只能交給鏈接器
當鏈接器進行鏈接的時候,首先決定各個目標文件在最終可執(zhí)行文件里的位置。然后訪問所有目標文件的地址重定義表,對其中記錄的地址進行重定向(加上一個偏移量,即該編譯單元在可執(zhí)行文件上的起始地址)。然后遍歷所有目標文件的未解決符號表,并且在所有的導出符號表里查找匹配的符號,并在未解決符號表中所記錄的位置上填寫實現(xiàn)地址,最后把所有的目標文件的內容寫在各自的位置上,就生成一個可執(zhí)行文件。鏈接的細節(jié)比較復雜,鏈接階段是單進程,無法并行加速,導致大項目鏈接極慢。
DQU是美團搜索使用的查詢理解平臺,內部包含了大量的模型、詞表、在代碼結構上,包含20多個Thrift文件 ,使用大量Boost處理函數(shù) ,同時引入了SF框架,公司第三方組件SDK以及分詞三個Submodule,各個模塊采用動態(tài)庫編譯加載的方式,模塊之間通過消息總線做數(shù)據(jù)的傳輸,消息總線是一個大的Event類,這樣這個類就包含了各個模塊需要的數(shù)據(jù)類型的定義,所以各個模塊都會引入Event頭文件,不合理的依賴關系造成這個文件被改動,幾乎所有的模塊都會重新編譯。
每個服務所面臨的編譯問題都有各自的特點,但是遇到問題的本質原因是類似的,結合編譯的過程和原理,我們從預編譯展開、頭文件依賴以及編譯過程耗時3個方面對DQU服務編譯問題進行了分析。
編譯展開分析就是通過C++的預編譯階段保留的.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")
編譯耗時的最直接原因就是編譯文件展開之后比較大,通過編譯展開后的文件大小和內容,通過預編譯展開分析能看到文件展開后的文件有40多萬行,發(fā)現(xiàn)有大量的Boost庫引用及頭文件引用造成的展開文件比較大,影響到編譯的耗時。通過這個方式能夠找到各個文件編譯耗時的共性,下圖是編譯展開后文件大小截圖。
頭文件依賴分析是從引用頭文件數(shù)量的角度來看代碼是否合理的一種分析方式,我們實現(xiàn)了一個腳本,用來統(tǒng)計頭文件的依賴關系,并且分析輸出頭文件依賴引用計數(shù),用來輔助判斷頭文件依賴關系是否合理。
(1) 頭文件引用總數(shù)結果統(tǒng)計
通過工具統(tǒng)計出編譯源文件直接和間接依賴的頭文件的總個數(shù),用來從頭文件引入數(shù)量上分析問題。
(2) 單個頭文件依賴關系統(tǒng)計
通過工具分析頭文件依賴關系,生成依賴關系拓撲圖,能夠直觀的看到依賴不合理的地方。
圖中包含引用層次關系,以及引用頭文件個數(shù)。
編譯耗時分段統(tǒng)計是從結果上看各個文件的編譯耗時以及各個編譯階段的耗時情況,這個是直觀的一個結果,正常情況下,是和文件展開大小以及頭文件引用個數(shù)是正相關的,cmake通過指定環(huán)境變量能打印出編譯和鏈接階段的耗時情況,通過這個數(shù)據(jù)能直觀的分析出耗時情況。
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CMAKE_COMMAND} -E time") set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK "${CMAKE_COMMAND} -E time")
編譯耗時結果輸出:
通過上面的工具分析能拿到幾個編譯數(shù)據(jù):
① 頭文件依賴關系及個數(shù)。 ② 預編譯展開大小及內容。 ③ 各個文件編譯耗時。 ④ 整體鏈接耗時。 ⑤ 可以計算出編譯并行度。
通過這幾個數(shù)據(jù)的輸入我們考慮可以做個自動化分析工具,找出優(yōu)化點以及界面化展示?;谶@個目的,我們建設了全流程自動化分析工具,能夠自動分析耗時共性問題以及TopN耗時文件。分析工具處理流程如下圖所示:
(1) 整體統(tǒng)計分析效果
具體字段說明:
① cost_time 編譯耗時,單位是秒。 ② file_compile_size,編譯中間文件大小,單位是M。 ③ file_name,文件名稱。 ④ include_h_nums,引入頭文件個數(shù),單位是個。 ⑤ top_h_files_info, 引入最多的TopN頭文件。
(2)Top10 編譯耗時文件統(tǒng)計
用來展示統(tǒng)計編譯耗時最久的TopN文件,N可以自定義指定。
(3)Top10編譯中間文件大小統(tǒng)計
通過統(tǒng)計和展示編譯文件大小,用來判斷這塊是否符合預期,這個是和編譯耗時一一對應的。
(4)Top10引入最多頭文件的頭文件統(tǒng)計
(5)Top10頭文件重復次數(shù)統(tǒng)計
目前,這個工具支持一鍵化生成編譯耗時分析結果,其中幾個小工具,比如依賴文件個數(shù)工具已經(jīng)集成到公司的上線集成測試流程中,通過自動化工具檢查代碼改動對編譯耗時的影響,工具的建設還在不斷迭代優(yōu)化中,后續(xù)會集成到公司的MCD平臺中,可以自動分析來定位編譯耗時長的問題,解決其它部門編譯耗時問題。
通過運用上述相關工具,我們能夠發(fā)現(xiàn)Top10編譯耗時文件的共性,比如都依賴消息總線文件platform_query_analysis_enent.h,這個文件又直接間接引入2000多個頭文件,我們重點優(yōu)化了這類文件,通過工具的編譯展開,找出了Boost使用、模板類展開、Thrift頭文件展開等共性問題,并針對這些問題做專門的優(yōu)化。此外,我們也使用了一些業(yè)內通用的編譯優(yōu)化方案,并取得了不錯的效果。下面詳細介紹我們采用的各種優(yōu)化方案。
業(yè)內有不少通用編譯加速工具(方案),無需侵入代碼就能提高編譯速度,非常值得嘗試。
(1)并行編譯
在Linux平臺上一般使用GNU的Make工具進行編譯,在執(zhí)行make命令時可以加上-j
參數(shù)增加編譯并行度,如make -j 4
將開啟4個任務。在實踐中我們并不將該參數(shù)寫死,而是通過$(nproc)
方法動態(tài)獲取編譯機的CPU核數(shù)作為編譯并發(fā)度,從而最大限度利用多核的性能優(yōu)勢。
(2)分布式編譯
使用分布式編譯技術,比如利用Distcc和Dmucs構建大規(guī)模、分布式C++編譯環(huán)境,Linux平臺利用網(wǎng)絡集群進行分布式編譯,需要考慮網(wǎng)絡時延與網(wǎng)絡穩(wěn)定性。分布式編譯適合規(guī)模較大的項目,比如單機編譯需要數(shù)小時甚至數(shù)天。DQU服務從代碼規(guī)模以及單機編譯時長來說,暫時還不需要使用分布式的方式來加速,具體細節(jié)可以參考Distcc官方文檔說明。
(3)預編譯頭文件
PCH(Precompiled Header),該方法預先將常用頭文件的編譯結果保存起來,這樣編譯器在處理對應的頭文件引入時可以直接使用預先編譯好的結果,從而加快整個編譯流程。PCH是業(yè)內十分常用的加速編譯的方法,且大家反饋效果非常不錯。在我們的項目中,由于涉及到很多Shared Library的編譯生成,而Shared Library相互之間無法共享PCH,因此沒有取得預想效果。
(4)CCache
CCache(Compiler Cache是一個編譯緩存工具,其原理是將cpp的編譯結果保存在文件緩存中,以后編譯時若對應文件無變動可直接從緩存中獲取編譯結果。需要注意的是,Make本身也有一定緩存功能,當目標文件已編譯(且依賴無變化)時,若源文件時間戳無變化也不會再次編譯;但CCache是按文件內容做的緩存,且同一機器的多個項目可以共享緩存,因此適用面更大。
(5)Module編譯
如果你的項目是用C++ 20進行開發(fā)的,那么恭喜你,Module編譯也是一個優(yōu)化編譯速度的方案,C++20之前的版本會把每一個cpp當做一個編譯單元處理,會存在引入的頭文件被多次解析編譯的問題。而Module的出現(xiàn)就是解決這一問題,Module不再需要頭文件(只需要一個模塊文件,不需要聲明和實現(xiàn)兩個文件),它會將你的(.ixx 或者 .cppm)模塊實體直接編譯,并自動生成一個二進制接口文件。import和include預處理不同,編譯好的模塊下次import的時候不會重復編譯,可以大幅度提高編譯器的效率。
(6)自動依賴分析
Google也推出了開源的Include-What-You-Use工具(簡稱IWYU),基于Clang的C/C++工程冗余頭文件檢查工具。IWYU依賴Clang編譯套件,使用該工具可以掃描出文件依賴問題,同時該工具還提供腳本解決頭文件依賴問題,我們嘗試搭建了這套分析工具,這個工具也提供自動化頭文件解決方案,但是由于我們的代碼依賴比較復雜,有動態(tài)庫、靜態(tài)庫、子倉庫等,這個工具提供的優(yōu)化功能不能直接使用,其它團隊如果代碼結構比較簡單的話,可以考慮使用這個工具分析優(yōu)化,會生成如下結果文件,指導哪些頭文件需要刪除。
>>> 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+#include // for BOOST_CLASS_VERSION +#include // for string +#include // for vector + +#include "qa/data/file_buffer.h" // for REG_TEMPLATE_FILE_HANDLER
(1)前置類型聲明
通過分析頭文件引用統(tǒng)計,我們發(fā)現(xiàn)項目中被引用最多的是總線類型Event,而該類型中又放置了各種業(yè)務需要的成員,示例如下:
#include “a.h” #include "b.h" class Event { // 業(yè)務A, B, C ... A1 a1; A2 a2; // ... B1 b1; B2 b2; // ... };
這導致Event中包含了數(shù)量龐大的頭文件,在頭文件展開后,文件大小達到15M;而各種業(yè)務都會需要使用Event,自然會嚴重拖累編譯性能。
我們通過前置類型聲明來解決這個問題,即不引入對應類型的頭文件,只做前置聲明,在Event中只使用對應類型的指針,如下所示:
class A2; // ... class Event { // 業(yè)務A, B, C ... shared_ptra1; shared_ptr a2; // ... shared_ptr b1; shared_ptr b2; // ... };
只有在真正使用對應成員變量時,才需要引入對應頭文件;這樣真正做到了按需引入頭文件。
(2)外部模板
由于模板被使用時才會實例化這一特性,相同的實例可以出現(xiàn)在多個文件對象中。編譯器要對每一處模板進行實例化,鏈接器還要移除重復的實例化代碼。當在廣泛使用模板的項目中,編譯器會產(chǎn)生大量的冗余代碼,這會極大地增加編譯時間和鏈接時間。C++ 11新標準中可以通過外部模板來避免。
// util.h templatevoid max(T) { ... }
// A.cpp extern template void max(int); #include "util.h" template void max (int); // 顯式地實例化 void test1() { max(1); }
在編譯A.cpp的時候,實例化出一個 max
// B.cpp #include "util.h" extern template void max(int); // 外部模板的聲明 void test2() { max(2); }
在編譯B.cpp的時候,就不再生成 max
(3)多態(tài)替換模板使用
我們的項目重度使用詞典相關操作,如加載詞典、解析詞典、匹配詞典(各種花式匹配),這些操作都是通過Template模板擴展支持各種不同類型的詞典。據(jù)統(tǒng)計,詞典的類型超過150個,這也造成模板展開的代碼量膨脹。
templateclass Dict { public: // 匹配key和condition,賦值給record bool match(const string &key, const string &condition, R &record); // 對每種類型的Record都會展開一次 private: map dict; };
幸運的是,我們詞典的絕大部分操作都可以抽象出幾類接口,因此可以只實現(xiàn)針對基類的操作:
class Record { // 基類 public: virtual bool match(const string &condition); // 派生類需實現(xiàn) }; class Dict { public: shared_ptrmatch(const string &key, const string &condition); // 使用方傳入派生類的指針即可 private: map > dict; };
通過繼承和多態(tài),我們有效避免了大量的模板展開。需要注意的是,使用指針作為Map的Value會增加內存分配的壓力,推薦使用Tcmalloc或Jemalloc替換默認的Ptmalloc優(yōu)化內存分配。
(4)替換Boost庫
Boost是一個廣泛使用的基礎庫,涵蓋了大量常用函數(shù),十分方便、好用,然而也存在一些不足之處。一個顯著缺點是其實現(xiàn)采用了hpp的形式,即聲明和實現(xiàn)均放在頭文件中,這會造成預編譯展開后十分巨大。
// 字符串操作是常用功能,僅僅引入該頭文件展開大小就超過4M #include// 與此相對的,引入多個STL的頭文件,展開后僅僅只有1M #include #include
在我們項目中主要使用的Boost函數(shù)不超過二十個,部分可以在STL中找到替代,部分我們手動做了實現(xiàn),使得項目從重度依賴Boost轉變成絕大部分達到Boost-Free,大大降低了編譯的負擔。
(5)預編譯
代碼中有一些平常改動比較少,但是對編譯耗時產(chǎn)生一定的影響,比如Thrift生成的文件,模型庫文件以及Common目錄下的通用文件,我們采取提起預編譯成動態(tài)庫,減少后續(xù)文件的編譯耗時,也解決了部分編譯依賴。
(6)解決編譯依賴,提高編譯并行度
在我們項目中有大量模塊級別的動態(tài)庫文件需要編譯,cmake文件指定的編譯依賴關系在一定程度上限制了編譯并行度的執(zhí)行。
比如下面這個場景,通過合理設置庫文件依賴關系,可以提高編譯并行度。
我們通過32C、64G內存機器做了編譯耗時優(yōu)化前后的效果對比,統(tǒng)計結果如下:
編譯優(yōu)化是一件“逆水行舟”的事情,開發(fā)人員總是傾向于不斷增加新的功能、新的庫乃至新的框架,而要刪除舊代碼、舊庫、下線舊框架總是困難重重(相信一線開發(fā)人員一定深有體會)。因此,如何守住之前取得的優(yōu)化成果也是至關重要的。我們在實踐中有以下幾點體會:
代碼審核是困難的(引起編譯耗時增加的改動,往往無法通過審核代碼直觀地發(fā)現(xiàn))。
工具、流程才值得依賴。
關鍵在于控制增量。
我們發(fā)現(xiàn),cpp文件的編譯耗時,和其預編譯展開文件(.ii)大小呈正相關(絕大部分情況下);對每一個上線版本,將其所有cpp文件的預編譯展開大小記錄下來,就形成了其編譯指紋(CF,Compile Fingerprint)。通過比較相鄰兩個版本的CF,就能較準確的知道新版帶來的編譯耗時主要由哪些改動引入,并可以進一步分析耗時上漲是否合理,是否有優(yōu)化空間。
我們將該種方式制作成腳本工具并引入上線流程,從而能夠很清楚的了解每次代碼發(fā)版帶來的編譯性能影響,并有效地幫助我們守住前期的優(yōu)化成果。
DQU項目是美團搜索業(yè)務環(huán)節(jié)中重要的一環(huán),該系統(tǒng)需要對接20+RPC、數(shù)十個模型、加載超過300個詞典,使用內存數(shù)十G,日均響應請求超過20億的大型C++服務。在業(yè)務高速迭代的情況,冗長的編譯時間為開發(fā)同學帶來較大的困擾,一定程度上制約了開發(fā)效率。最終我們通過編譯優(yōu)化分析工具建設,結合采用了通用編譯優(yōu)化加速方案和代碼層面的優(yōu)化,將DQU的編譯時間縮短了70%,并通過引CCache等手段,使得本地開發(fā)的編譯,能夠在100s內完成,給開發(fā)團隊節(jié)省了大量的時間。
在取得階段性成果之后,我們總結整個問題解決的過程,并沉淀出一些分析方法、工具以及流程規(guī)范。這些工具在后續(xù)的開發(fā)迭代過程中,能夠快速有效地檢測新的代碼變更帶來的編譯時間變化,并成為了我們的上線流程檢查中的一環(huán)檢測標準。這一點與我們以往一次性的或者針對性的編譯優(yōu)化,產(chǎn)生了很大的區(qū)別。畢竟代碼的維護是一個持久的過程,系統(tǒng)化的解決這一問題,不只是需要有效的方法和便捷的工具,更需要一個標準化的,規(guī)范化的上線流程來保持成果。
到此,相信大家對“C++服務編譯耗時優(yōu)化原理是什么”有了更深的了解,不妨來實際操作一番吧!這里是創(chuàng)新互聯(lián)網(wǎng)站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續(xù)學習!