戰(zhàn)指南:從架構(gòu)拆解到源碼構(gòu)建與二次開(kāi)發(fā))
1. llvm-project到底是什么——先解決這玩意值不值得搞的問(wèn)題接觸編譯器的人早晚都會(huì)撞上llvm-project這個(gè)倉(cāng)庫(kù)。我第一次真正去翻它的源碼不是出于興趣是被一個(gè)項(xiàng)目逼的要在自研芯片上跑一套定制的編譯工具鏈還得把上游的優(yōu)化能力吃下來(lái)。那時(shí)候打開(kāi)github一看好家伙這個(gè)倉(cāng)庫(kù)大到讓人頭皮發(fā)麻幾十個(gè)子項(xiàng)目堆在一起光clone就得等半天。先給沒(méi)接觸過(guò)的朋友說(shuō)清楚。llvm-project是LLVM編譯器基礎(chǔ)設(shè)施的官方主倉(cāng)庫(kù)GitHub上那個(gè)star數(shù)夸張的倉(cāng)庫(kù)就是它。它不是某個(gè)具體的編譯器而是整套編譯相關(guān)工具鏈的集合核心構(gòu)件包括LLVM提供中端優(yōu)化和后端代碼生成、ClangC/C/Objective-C前端、LLD鏈接器、libcC標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)、compiler-rt運(yùn)行時(shí)庫(kù)、MLIR多層級(jí)中間表示框架、OpenMP運(yùn)行時(shí)庫(kù)等等??梢哉f(shuō)現(xiàn)代編譯領(lǐng)域的絕大多數(shù)技術(shù)突破要么直接在這個(gè)倉(cāng)庫(kù)里落地要么就是從這里派生出去的。那這個(gè)項(xiàng)目能做什么最直接的答案是三件事第一你可以基于它構(gòu)建出一套完整的C/C工具鏈替代或者定制GCC的工作第二你可以在它的中端和后端之上做二次開(kāi)發(fā)實(shí)現(xiàn)自己的優(yōu)化pass或者支持新的處理器架構(gòu)第三你可以用它的庫(kù)來(lái)構(gòu)建靜態(tài)分析工具、代碼格式化工具、調(diào)試器前端甚至是一門(mén)新語(yǔ)言的編譯器。適合誰(shuí)看想深入理解現(xiàn)代編譯器工作原理的學(xué)生工作中要做工具鏈定制或交叉編譯的工程師以及對(duì)編程語(yǔ)言實(shí)現(xiàn)感興趣但不想從零寫(xiě)代碼生成器的開(kāi)發(fā)者這篇文章都能給你一個(gè)可以照著做的路徑。我準(zhǔn)備從項(xiàng)目本身的構(gòu)成講起再拆核心架構(gòu)然后給你完整的源碼構(gòu)建流程最后把我在實(shí)際使用中踩過(guò)的坑和排查思路全部整理出來(lái)。這篇文章不是教你怎么裝一個(gè)Clang然后console.log一下hello world而是帶著你理解這個(gè)龐大項(xiàng)目?jī)?nèi)部是怎么組織、怎么構(gòu)建、怎么為二次開(kāi)發(fā)鋪路的。2. 核心架構(gòu)拆解三段式設(shè)計(jì)與Clang的定位2.1 LLVM的三段式架構(gòu)救了多少編譯器要理解llvm-project為什么能統(tǒng)治編譯器基礎(chǔ)設(shè)施這個(gè)領(lǐng)域必須先看它的三段式架構(gòu)設(shè)計(jì)。傳統(tǒng)編譯器如早期的GCC走的是直接把前端語(yǔ)法樹(shù)翻譯成后端指令的路線每支持一門(mén)新語(yǔ)言幾乎都要從頭寫(xiě)一套包含優(yōu)化和代碼生成的完整編譯器。而LLVM把編譯過(guò)程拆成三個(gè)階段前端負(fù)責(zé)把源代碼翻譯成中間表示IR中端基于IR做平臺(tái)無(wú)關(guān)的優(yōu)化后端把優(yōu)化后的IR變成目標(biāo)機(jī)器的匯編或機(jī)器碼。這個(gè)拆分的好處是邏輯解耦非常徹底。前端開(kāi)發(fā)者只需要把語(yǔ)言翻譯成合格的LLVM IR立刻就能享受中端所有的優(yōu)化能力和后端所有支持平臺(tái)——x86、ARM、RISC-V、GPU等。后端開(kāi)發(fā)者不需要關(guān)心語(yǔ)言層面的事情專(zhuān)注把IR映射到目標(biāo)指令集。這帶來(lái)的生態(tài)效應(yīng)是巨大的今天大模型領(lǐng)域的Triton、機(jī)器學(xué)習(xí)領(lǐng)域的TVM核心思路都受了這三段式的啟發(fā)所以搞懂LLVM的架構(gòu)對(duì)理解很多現(xiàn)代編譯類(lèi)框架都有幫助。Clang就是LLVM的前端實(shí)現(xiàn)它的定位是把C/C/Objective-C代碼解析、語(yǔ)義分析后生成AST抽象語(yǔ)法樹(shù)再降級(jí)成LLVM IR。很多人一開(kāi)始會(huì)把Clang和LLVM混為一談實(shí)際在llvm-project倉(cāng)庫(kù)里Clang是頂層的一個(gè)子目錄項(xiàng)目和LLVM核心是平級(jí)的關(guān)系。Clang相比GCC有個(gè)很突出的優(yōu)點(diǎn)就是模塊化程度高、代碼清晰另一個(gè)更貼近用戶(hù)的特點(diǎn)是錯(cuò)誤提示非常友好能直接指出出錯(cuò)的具體位置和可能的修復(fù)建議這也是很多開(kāi)發(fā)者一用上Clang就回不去的原因。2.2 中端優(yōu)化與LLVM IR的作用LLVM IR是整個(gè)基礎(chǔ)設(shè)施的靈魂它是一種靜態(tài)單賦值SSA形式的中間表示也就是說(shuō)每個(gè)變量只被賦值一次。這聽(tīng)起來(lái)有點(diǎn)繞但理解之后你就知道它為什么能支撐那么強(qiáng)大的優(yōu)化能力。SSA形式讓數(shù)據(jù)流分析變得非常直觀比方說(shuō)一個(gè)變量的定義和使用關(guān)系在SSA下就是一目了然的死代碼消除、常量傳播這類(lèi)優(yōu)化做起來(lái)效率極高。IR有三種存儲(chǔ)形態(tài)內(nèi)存中的表示、可讀的文本形式.ll文件、二進(jìn)制位碼形式.bc文件。這三者可以互相轉(zhuǎn)換而且轉(zhuǎn)換是無(wú)損的。這對(duì)調(diào)試和測(cè)試來(lái)說(shuō)實(shí)在太重要了你可以把你的源碼先編譯成.ll文本文件打開(kāi)一眼看到中間表示長(zhǎng)什么樣找到它是否符合預(yù)期也可以寫(xiě)一個(gè)優(yōu)化pass然后跑在文本IR上驗(yàn)證效果全程不用碰機(jī)器碼。中端的優(yōu)化Pipeline里跑的是數(shù)百個(gè)Pass像EarlyCSE、InlineFunction、GVN、LoopUnroll、SLP向量化等這些Pass從IR入手做各種等價(jià)變換目標(biāo)是讓程序跑得更快、體積更小、功耗更低。llvm-project里大量代碼其實(shí)都集中在這層如果你想搞編譯器優(yōu)化方向的研究中端就是你最常待的地方。2.3 后端的指令選擇與寄存器分配IR經(jīng)過(guò)中端優(yōu)化后送到后端。這里要經(jīng)歷的環(huán)節(jié)包括指令選擇Instruction Selection、指令調(diào)度Scheduling、寄存器分配Register Allocation、指令布局Layout、編碼Emit等。傳統(tǒng)后端使用SelectionDAG來(lái)做指令選擇把IR轉(zhuǎn)成一個(gè)依賴(lài)圖再通過(guò)模式匹配來(lái)映射目標(biāo)指令。近些年LLVM發(fā)展了GlobalISel這個(gè)新的指令選擇框架在處理復(fù)雜指令和異構(gòu)后端的時(shí)候更靈活尤其是對(duì)GPU和DSP這類(lèi)非規(guī)則架構(gòu)GlobalISel在逐步取代老方案。寄存器分配是后端最經(jīng)典的難題之一。寄存器數(shù)量有限程序里的值卻很多LLVM默認(rèn)使用貪心寄存器分配器Greedy Register Allocator它會(huì)為每個(gè)虛擬寄存器嘗試找到一個(gè)物理寄存器在寄存器不夠的時(shí)候觸發(fā)溢寫(xiě)spill到內(nèi)存。這里面的啟發(fā)式算法直接決定了生成代碼的質(zhì)量差之毫厘就可能導(dǎo)致性能掉幾個(gè)百分點(diǎn)。如果你將來(lái)要做一個(gè)新的CPU后端通常的做法就是在llvm-project中新建一個(gè)后端目錄實(shí)現(xiàn)TargetMachine、TargetLowering、指令選擇、寄存器信息等接口。這個(gè)路徑已經(jīng)被無(wú)數(shù)項(xiàng)目驗(yàn)證過(guò)了RISC-V后端就是社區(qū)靠著這套機(jī)制在幾年內(nèi)快速成熟的。llvm-project的后端框架之所以值錢(qián)在于它提供了一個(gè)高度可擴(kuò)展的模版你不需要從零開(kāi)始造輪子只需要填好目標(biāo)平臺(tái)相關(guān)的關(guān)鍵信息。3. 實(shí)操?gòu)脑创a構(gòu)建llvm-project的正確姿勢(shì)3.1 構(gòu)建前的準(zhǔn)備磁盤(pán)、內(nèi)存、依賴(lài)我在第一次全量構(gòu)建llvm-project時(shí)就犯過(guò)一個(gè)很基本的錯(cuò)誤直接clone了master的分支然后cmake默認(rèn)配置開(kāi)始構(gòu)建結(jié)果編到一半磁盤(pán)滿(mǎn)了。llvm-project的歷史版本加上構(gòu)建產(chǎn)物非常吃磁盤(pán)空間這個(gè)事必須提前重視。首先倉(cāng)庫(kù)本體大小就很可觀git clone的默認(rèn)淺克隆深度是1這樣能省不少時(shí)間。但如果你要做開(kāi)發(fā)、要切分支、要看歷史提交還是建議完整克隆。我通常的做法是先淺克隆一份需要?dú)v史的時(shí)候再按需拉取。這不算偷懶而是節(jié)省時(shí)間的實(shí)用策略。其次依賴(lài)方面最基礎(chǔ)的有CMake建議3.20以上版本、Ninja一個(gè)比make更快更方便的構(gòu)建系統(tǒng)、C編譯器GCC、Clang均可要求支持C17標(biāo)準(zhǔn)。另外還需要zlib、libxml2等一些庫(kù)因?yàn)閘lvm-project里有些工具會(huì)依賴(lài)它們。Python也是必需的多個(gè)腳本和組件會(huì)在構(gòu)建時(shí)調(diào)用。在我用的Ubuntu環(huán)境上這一條命令就能裝完大部分依賴(lài)sudo apt update sudo apt install -y build-essential cmake ninja-build python3 python3-pip git zlib1g-dev libxml2-dev磁盤(pán)空間這一塊完整構(gòu)建所有子項(xiàng)目并且開(kāi)啟調(diào)試信息幾百GB都可能打不住但如果你只構(gòu)建LLVM和Clang并且關(guān)閉調(diào)試符號(hào)50GB以?xún)?nèi)基本夠用。我建議磁盤(pán)預(yù)留至少80GB避免構(gòu)建到一半才發(fā)現(xiàn)空間不夠的尷尬。內(nèi)存方面全量構(gòu)建LLVM和Clang是比較吃?xún)?nèi)存的尤其是鏈接階段的幾個(gè)大的可執(zhí)行文件比如clang和lld鏈接時(shí)可能吃掉幾個(gè)GB的內(nèi)存。如果機(jī)器內(nèi)存只有8GB建議限制并行度不然容易OOM具體后面會(huì)說(shuō)。3.2 CMake配置詳解關(guān)鍵參數(shù)llvm-project的構(gòu)建在大部分場(chǎng)景下走的是CMake這一個(gè)入口。在源碼根目錄下建立一個(gè)build目錄然后在這個(gè)目錄里執(zhí)行cmake命令來(lái)生成構(gòu)建文件這是標(biāo)準(zhǔn)的做法防止構(gòu)建產(chǎn)物污染源碼樹(shù)。我把非常常用的一套配置給你這幾乎是我每次給自有項(xiàng)目做定制工具鏈時(shí)的起始模板cmake -G Ninja ../llvm-project/llvm \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;libcxx;libcxxabi \ -DLLVM_TARGETS_TO_BUILDX86;AArch64;RISCV \ -DLLVM_ENABLE_ASSERTIONSON \ -DLLVM_CCACHE_BUILDON逐個(gè)解釋這些參數(shù)的意義-G Ninja指定使用Ninja生成構(gòu)建文件。Ninja并發(fā)效率比make高得多尤其在大型C項(xiàng)目上這是一個(gè)不用猶豫的選擇。-DCMAKE_BUILD_TYPERelease編譯類(lèi)型設(shè)為Release會(huì)啟用-O2優(yōu)化Debug模式生成的編譯器適合斷點(diǎn)調(diào)試但運(yùn)行速度慢得多且產(chǎn)物體積大幾倍。如果只是日常使用Release就夠了。-DLLVM_ENABLE_PROJECTS這個(gè)參數(shù)決定要構(gòu)建哪些LLVM外圍項(xiàng)目。Clang肯定要的lld鏈接器速度飛快值得擁有l(wèi)ibcxx和libcxxabi是C標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)想測(cè)試新標(biāo)準(zhǔn)特性的時(shí)候很有用。需要說(shuō)明的是這里的項(xiàng)目名用分號(hào)分隔。-DLLVM_TARGETS_TO_BUILD控制編譯器支持哪些目標(biāo)架構(gòu)。默認(rèn)會(huì)構(gòu)建所有平臺(tái)的后端費(fèi)時(shí)費(fèi)力實(shí)際上大多數(shù)人用不到那些嵌入式或小眾平臺(tái)。我平時(shí)只保留X86、AArch64和RISCV夠用且省時(shí)間。-DLLVM_ENABLE_ASSERTIONSON開(kāi)啟斷言。對(duì)普通使用者這個(gè)可以關(guān)掉因?yàn)镽elease下開(kāi)啟斷言不會(huì)提升編譯產(chǎn)物質(zhì)量但如果你在做編譯器開(kāi)發(fā)強(qiáng)烈建議打開(kāi)很多內(nèi)部不變量靠斷言來(lái)兜底。-DLLVM_CCACHE_BUILDON開(kāi)啟ccache緩存。增量開(kāi)發(fā)時(shí)有了ccache可以省掉大量重復(fù)編譯時(shí)間尤其是當(dāng)你切換分支或改了一點(diǎn)頭文件導(dǎo)致大范圍重編的時(shí)候ccache的效果立竿見(jiàn)影。還有一個(gè)常見(jiàn)參數(shù)是-DLLVM_PARALLEL_LINK_JOBS用它來(lái)控制鏈接任務(wù)的并行數(shù)。在內(nèi)存有限的情況下把它設(shè)成1或2可以避免多個(gè)大鏈接任務(wù)同時(shí)搶內(nèi)存。我有一次在16GB內(nèi)存的機(jī)器上構(gòu)建同時(shí)跑了8個(gè)鏈接作業(yè)直接物理內(nèi)存爆掉機(jī)器卡死。3.3 構(gòu)建與測(cè)試驗(yàn)證配置完成后執(zhí)行構(gòu)建ninja -j8數(shù)字8是并行度具體的值參考CPU核心數(shù)但不建議超過(guò)內(nèi)存能承受的上限。構(gòu)建過(guò)程會(huì)輸出大量編譯日志初次構(gòu)建需要等待較長(zhǎng)時(shí)間在常見(jiàn)配置下全量構(gòu)建LLVM和Clang在8核機(jī)器上大概需要20到30分鐘都是在可接受范圍內(nèi)的。構(gòu)建完成后二進(jìn)制在build/bin目錄下可以驗(yàn)證一下./build/bin/clang --version如果想跑一遍L(zhǎng)LVM的測(cè)試套件用以下命令ninja check-llvm check-clang這個(gè)會(huì)執(zhí)行大規(guī)模的單元測(cè)試和回歸測(cè)試。我第一次跑的時(shí)候發(fā)現(xiàn)有幾個(gè)測(cè)試失敗排查后確認(rèn)是環(huán)境問(wèn)題導(dǎo)致編譯器斷言被觸發(fā)后來(lái)調(diào)整了配置就正常了。測(cè)試套件對(duì)編譯器開(kāi)發(fā)者來(lái)說(shuō)是一道防線提交代碼之前至少要把相關(guān)的check跑綠不然很容易把上游已有的功能改壞。構(gòu)建一個(gè)小的測(cè)試程序驗(yàn)證生成的編譯器可以正常使用cat hello.c EOF #include stdio.h int main() { printf(hello llvm\n); return 0; } EOF ./build/bin/clang hello.c -o hello ./hello這里有個(gè)細(xì)節(jié)需要注意新建的Clang默認(rèn)可能找不到系統(tǒng)頭文件尤其在某些精簡(jiǎn)的Linux發(fā)行版上。一個(gè)快速的解決方法是安裝build-essential這類(lèi)包含完整頭文件和庫(kù)的開(kāi)發(fā)包或者讓Clang自動(dòng)探測(cè)GCC的安裝路徑來(lái)借用它的頭文件和庫(kù)配置。4. 構(gòu)建過(guò)程常見(jiàn)問(wèn)題與排查實(shí)錄4.1 內(nèi)存不足與OOM問(wèn)題這是llvm-project新手最容易碰到的坑。構(gòu)建過(guò)程中報(bào)c: internal compiler error: Killed (program cc1plus)很多時(shí)候就是內(nèi)存不足系統(tǒng)把編譯進(jìn)程給殺了。說(shuō)到底是因?yàn)榫幾g大型C文件很吃?xún)?nèi)存多個(gè)編譯任務(wù)并行時(shí)內(nèi)存使用成倍增長(zhǎng)。我給的排查思路是先確認(rèn)當(dāng)前系統(tǒng)的實(shí)際內(nèi)存和swap大小可以用free -h查看。如果內(nèi)存緊張把并行度降下來(lái)比如從-j8降到-j4甚至-j2。另外建議把-DLLVM_PARALLEL_LINK_JOBS1顯式加上。還有一個(gè)容易被忽略的技巧是使用-DCMAKE_BUILD_TYPERelease而不是DebugRelease下的優(yōu)化雖然耗編譯時(shí)間但運(yùn)行期的內(nèi)存占用反而小Debug模式下的代碼優(yōu)化較少編譯時(shí)內(nèi)存壓力更大在某些弱雞機(jī)器上確實(shí)會(huì)明顯吃力。如果實(shí)在內(nèi)存太小另一個(gè)思路是只構(gòu)建LLVM核心不加Clang。CLang的編譯目標(biāo)比較大單獨(dú)構(gòu)建LLVM庫(kù)和工具鏈的話(huà)資源開(kāi)銷(xiāo)會(huì)低很多。4.2 頭文件與Python依賴(lài)導(dǎo)致失敗構(gòu)建或者運(yùn)行時(shí)偶爾會(huì)遇到找不到頭文件的報(bào)錯(cuò)比如fatal error: stdio.h file not found。這種情況通常說(shuō)明系統(tǒng)的頭文件路徑?jīng)]有被Clang正確識(shí)別。在Linux上最省事的方法是安裝GCC的開(kāi)發(fā)包Clang會(huì)檢測(cè)到GCC的安裝位置然后借用它的libstdc頭文件和系統(tǒng)C庫(kù)頭文件。有一個(gè)萬(wàn)能驗(yàn)證命令echo | ./build/bin/clang -E -v -x c -它會(huì)輸出Clang實(shí)際搜索的所有頭文件路徑排查起來(lái)非常直觀。如果發(fā)現(xiàn)路徑里缺少了關(guān)鍵的系統(tǒng)頭文件目錄可以通過(guò)-DCMAKE_C_COMPILER和-DCMAKE_CXX_COMPILER指定系統(tǒng)已有的編譯器來(lái)重新配置或者在編譯時(shí)手動(dòng)加-isystem參數(shù)指向正確的頭文件目錄。Python依賴(lài)引發(fā)的構(gòu)建失敗同樣常見(jiàn)比如找不到Python3Development相關(guān)組件或者某個(gè)腳本執(zhí)行時(shí)報(bào)模塊缺失。解決方法是確保系統(tǒng)安裝了python3-dev包并且CMake能夠找到它。如果你用conda環(huán)境有可能CMake優(yōu)先找到了conda里的Python但那個(gè)環(huán)境的庫(kù)不完整這時(shí)可以顯式指定-DPython3_EXECUTABLE$(which python3)。4.3 鏈接階段卡住或失敗鏈接失敗是另一個(gè)高頻問(wèn)題。報(bào)錯(cuò)類(lèi)型多種多樣比如/usr/bin/ld: cannot find -lz說(shuō)明缺少zlib庫(kù)undefined reference to xxx說(shuō)明相關(guān)的庫(kù)沒(méi)有鏈接進(jìn)來(lái)或者某些功能被裁剪掉了。解決這類(lèi)問(wèn)題的思路分幾步。第一步確認(rèn)系統(tǒng)庫(kù)已經(jīng)安裝sudo apt install zlib1g-dev libxml2-dev這類(lèi)命令按需補(bǔ)齊。第二步檢查CMake配置里是否有裁剪選項(xiàng)比如你只開(kāi)啟了Clang但構(gòu)建的某個(gè)工具依賴(lài)libxml2那么缺失庫(kù)就會(huì)暴露出來(lái)。第三步看具體是哪個(gè)鏈接命令失敗把CMake生成的build.ninja或build/CMakeCache.txt里相關(guān)變量翻出來(lái)比如CMAKE_EXE_LINKER_FLAGS手動(dòng)補(bǔ)充庫(kù)路徑或庫(kù)名。鏈接卡住看起來(lái)沒(méi)報(bào)錯(cuò)但一直不動(dòng)通常是內(nèi)存不夠或者IO瓶頸。我遇到過(guò)一次是在機(jī)械硬盤(pán)上鏈接一個(gè)巨大的可執(zhí)行文件等了很多分鐘都沒(méi)完換到SSD之后速度快了好幾倍這不是配置問(wèn)題而是存儲(chǔ)設(shè)備的差距。4.4 新舊Pass管理器帶來(lái)的API差異這個(gè)問(wèn)題主要影響做二次開(kāi)發(fā)的人。LLVM從某個(gè)版本開(kāi)始默認(rèn)使用New Pass Manager新Pass管理器但如果你的項(xiàng)目還基于舊的Legacy Pass Manager的API來(lái)寫(xiě)Pass編譯時(shí)可能報(bào)一堆兼容性錯(cuò)誤。這個(gè)無(wú)論是在構(gòu)建llvm-project本體還是在你自己的分析工具里都容易遇到。最簡(jiǎn)單的排查辦法是看LLVM版本然后查對(duì)應(yīng)版本的官方遷移文檔。寫(xiě)新代碼建議直接基于New PM接口因?yàn)樯嫌蔚膬?yōu)化Pipeline已經(jīng)全面轉(zhuǎn)向新框架老接口未來(lái)盡可能縮減。還要注意一點(diǎn)llvm-project的版本迭代很快main分支和release分支的API差異很大做開(kāi)發(fā)一定要鎖定一個(gè)版本分支不要讓項(xiàng)目追蹤main分支否則今天能編過(guò)下周可能就編不過(guò)了。我習(xí)慣把項(xiàng)目依賴(lài)的LLVM版本用git submodule固定下來(lái)這樣團(tuán)隊(duì)協(xié)作時(shí)大家構(gòu)建的環(huán)境一致排查問(wèn)題也不會(huì)因?yàn)榘姹酒贫靵y。5. llvm-project能拿來(lái)做什么應(yīng)用場(chǎng)景與二次開(kāi)發(fā)方向5.1 自制語(yǔ)言與編譯器的構(gòu)建塊如果你對(duì)編程語(yǔ)言實(shí)現(xiàn)感興趣但又不想從零寫(xiě)整個(gè)編譯器生態(tài)llvm-project這一套可以讓你繞開(kāi)大量重復(fù)造輪子的工作。新語(yǔ)言的前端可以生成LLVM IR之后就全部交給既有優(yōu)化和后端。我之前看過(guò)一個(gè)朋友做的小型靜態(tài)類(lèi)型語(yǔ)言前端解析和類(lèi)型檢查花了幾個(gè)月后端幾乎沒(méi)碰——直接生成IR、調(diào)用LLVM的JIT引擎就拿到了解釋執(zhí)行和即時(shí)編譯能力。LLVM庫(kù)提供了一套完整的JIT API比如ORC JIT可以實(shí)現(xiàn)運(yùn)行時(shí)把IR編譯成機(jī)器碼這對(duì)動(dòng)態(tài)語(yǔ)言、腳本引擎、甚至SQL執(zhí)行引擎的加速都有很大價(jià)值。5.2 靜態(tài)分析與代碼質(zhì)量工具llvm-project擁有豐富的靜態(tài)分析基礎(chǔ)設(shè)施。Clang Static Analyzer做的是路徑敏感分析能發(fā)現(xiàn)空指針解引用、資源泄漏這類(lèi)深層次問(wèn)題Clang-Tidy則是一堆基于語(yǔ)法樹(shù)的快速檢查規(guī)則適合做風(fēng)格和局部正確性的檢查。這些工具的底層都借助了LibTooling和Clang AST Matcher這些庫(kù)。如果你要給自己的項(xiàng)目定制一套代碼規(guī)范檢查規(guī)則可以基于Clang-Tidy框架寫(xiě)一個(gè)自定義 checker不需要維護(hù)一個(gè)完整的編譯器就能做解析和模式匹配。我在實(shí)際工作中用到的一個(gè)場(chǎng)景是給一個(gè)大型C代碼庫(kù)批量查找某種不安全的API使用模式。用AST Matcher寫(xiě)了一個(gè)幾十行的工具幾秒鐘掃完整個(gè)倉(cāng)庫(kù)效率遠(yuǎn)高于人工code review。靜態(tài)分析這個(gè)方向llvm-project給的起點(diǎn)已經(jīng)很高關(guān)鍵是你愿不愿意學(xué)它的接口。5.3 MLIR與編譯器基礎(chǔ)設(shè)施未來(lái)MLIRMulti-Level Intermediate Representation是llvm-project里近幾年發(fā)展最猛烈的項(xiàng)目之一。早期它主要服務(wù)于機(jī)器學(xué)習(xí)編譯器的多層抽象需求后來(lái)大家發(fā)現(xiàn)這個(gè)框架對(duì)任何需要多層IR變換的領(lǐng)域都很有價(jià)值比如HPC、FPGA綜合、領(lǐng)域特定語(yǔ)言DSL等。MLIR解決的是「一層IR不夠用」的問(wèn)題既有接近源碼的高層抽象方便做領(lǐng)域相關(guān)的優(yōu)化又有接近硬件的低層抽象方便做指令映射和調(diào)度。它可以讓你定義自己的Dialect方言再寫(xiě)Dialect之間的轉(zhuǎn)換Pass。今天很多火爆的AI編譯框架底層都有MLIR的影子。如果你要吃編譯器這碗飯llvm-project中的MLIR子項(xiàng)目絕對(duì)值得投入時(shí)間。它不是另一個(gè)跟LLVM孤立的技術(shù)而是整個(gè)基礎(chǔ)設(shè)施向更廣泛場(chǎng)景演進(jìn)的體現(xiàn)。5.4 工具鏈集成與環(huán)境落地許多企業(yè)級(jí)項(xiàng)目并不需要從源碼構(gòu)建LLVM而是直接把llvm-project的發(fā)布版本集成進(jìn)自己的工具鏈里。比如在CI流水線里使用Clang-Tidy做代碼檢查、用Clang-Format統(tǒng)一代碼風(fēng)格、用LLD加速鏈接過(guò)程。這里用到的產(chǎn)品和llvm-project同源只是使用方式不同。如果你需要定制比如要加一個(gè)自定義的優(yōu)化Pass、給某款特殊芯片生成專(zhuān)屬指令序列就沒(méi)有辦法回避源碼構(gòu)建和二次開(kāi)發(fā)。最典型的路徑是拉取llvm-project源碼在llvm/lib/Transforms下新建一個(gè)Pass目錄注冊(cè)進(jìn)Pass Pipeline編譯后用opt工具加載并運(yùn)行。這套流程雖然初看門(mén)檻不低但把基礎(chǔ)打通之后后續(xù)基本上都是復(fù)現(xiàn)和迭代的問(wèn)題。我個(gè)人在實(shí)際操作中的體會(huì)是llvm-project最大的學(xué)習(xí)價(jià)值不是某一條命令或者某一個(gè)工具而是它展示了「編譯器」這種極度復(fù)雜的系統(tǒng)如何通過(guò)合理的模塊劃分、嚴(yán)格的接口約定、可持續(xù)的測(cè)試體系變成一個(gè)成千上萬(wàn)人協(xié)作卻依然保持高效迭代的項(xiàng)目。最后再分享一個(gè)小技巧開(kāi)工之前先看llvm-project的官方文檔和郵件列表存檔很多你準(zhǔn)備花一周摸索的問(wèn)題其實(shí)社區(qū)早就討論過(guò)并給出了結(jié)論。還有為你的開(kāi)發(fā)環(huán)境安裝一套版本管理工具比如ccache再加distcc大型編譯任務(wù)能舒服不少。