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