:從C++生成、修改到優(yōu)化驗證)
簡介本資源是一個面向C開發(fā)者與編譯器學(xué)習者的LLVM IR生成實踐項目聚焦于通過原生C API手動生成LLVM中間表示適用于編譯原理課程實踐、自研編譯器前端開發(fā)及LLVM工具鏈入門。項目完整覆蓋LLVM環(huán)境初始化、Module構(gòu)建、函數(shù)與基本塊定義、SSA指令插入及.ll文件輸出等核心流程幫助讀者深入理解編譯器后端代碼生成機制。壓縮包共50個文件含25個C源碼.cc實現(xiàn)IR構(gòu)造邏輯、16個頭文件.h封裝AST與代碼生成類、1個.ll示例輸出、5個說明文本及配套構(gòu)建腳本整體僅23KB輕量易讀目錄結(jié)構(gòu)清晰分為ast、parsing、codegen等模塊。目前已有463人學(xué)習下載可直接編譯運行獲取可驗證的IR生成范例、類型系統(tǒng)設(shè)計思路與基礎(chǔ)控制流構(gòu)建方法是掌握LLVM C綁定API的優(yōu)質(zhì)入門實操材料。1. LLVM IR 生成演示不是“編譯器黑匣子”而是你能親手拆開、改寫、再注入的中間表示層你寫了一行int a b c;GCC 或 Clang 編譯完就直接吐出機器碼——這過程像把原料塞進絞肉機出來就是香腸你連刀片朝哪邊轉(zhuǎn)都不知道。但 LLVM IR 不一樣它是一份人類可讀、結(jié)構(gòu)清晰、語義明確的匯編級中間語言既不是 C 源碼也不是 x86 二進制而是一份“編譯器能懂、程序員也能 debug”的通用契約。這個llvm-ir-dimostrazione項目就是一套精簡但完整的實戰(zhàn)切片——它不教你如何從零寫一個 LLVM 后端而是用 3 個真實 C 函數(shù)含指針運算、循環(huán)、條件跳轉(zhuǎn)逐行生成.ll文件再用lli直接執(zhí)行、用opt做常量傳播、用llc翻譯成匯編最后反向驗證 IR 修改是否生效。適合正在學(xué)編譯原理的研究生、想搞性能調(diào)優(yōu)的 C 工程師或是被-O2優(yōu)化結(jié)果搞懵、急需“看到編譯器到底干了什么”的一線開發(fā)者。它不依賴完整 LLVM 構(gòu)建環(huán)境只要系統(tǒng)裝了clang和llvm-toolsUbuntu/Debian 下apt install llvm clang即可5 分鐘內(nèi)就能跑通第一條 IR 輸出。2. 從 C 源碼到 .ll 文件三步生成法與 clang -S 的隱藏參數(shù)2.1 為什么不用clang -O0 -SIR 生成必須繞過前端“美化”陷阱很多初學(xué)者直接對.cpp文件執(zhí)行clang -S -emit-llvm main.cpp結(jié)果發(fā)現(xiàn)生成的.ll里堆滿%0 alloca i32, align 4和store i32 42, i32* %0, align 4這類冗余指令——這不是 IR 本身的問題而是 clang 默認啟用-fno-omit-frame-pointer和-mllvm -enable-indvar等隱式優(yōu)化開關(guān)導(dǎo)致即使-O0也會插入調(diào)試友好但語義模糊的棧幀操作。llvm-ir-dimostrazione的核心做法是強制禁用所有前端插樁直通 LLVM IR 生成管道。正確命令是clang -x c -stdc17 -O0 -S -emit-llvm -Xclang -disable-O0-optnone \ -Xclang -disable-llvm-passes -Xclang -disable-llvm-verifier \ -o example.ll example.cpp提示-Xclang是傳遞給 clang 前端的開關(guān)不是給 LLVM 的-disable-O0-optnone關(guān)鍵在于繞過 clang 在-O0下自動插入optnone屬性該屬性會阻止后續(xù)opt工具做任何優(yōu)化-disable-llvm-passes并非關(guān)閉所有 pass而是禁用 clang 自帶的 IR 預(yù)處理 pass如 SROA 初步分析確保輸出最原始、未修飾的 IR。2.2 示例函數(shù)用指針算術(shù)和 for 循環(huán)構(gòu)造 IR 可視化錨點項目中example.cpp包含三個典型函數(shù)每個都設(shè)計為在 IR 中產(chǎn)生明確、易追蹤的模式// example.cpp int simple_add(int x, int y) { return x y; } int array_sum(int* arr, int len) { int sum 0; for (int i 0; i len; i) { sum arr[i]; } return sum; } int conditional_max(int a, int b) { return (a b) ? a : b; }編譯后example.ll中對應(yīng)片段如下截取array_sum主體define i32 array_sum(i32* %arr, i32 %len) #0 { entry: %sum alloca i32, align 4 %i alloca i32, align 4 store i32 0, i32* %sum, align 4 store i32 0, i32* %i, align 4 br label %for.cond for.cond: ; preds %for.inc, %entry %0 load i32, i32* %i, align 4 %cmp icmp slt i32 %0, %len br i1 %cmp, label %for.body, label %for.end for.body: ; preds %for.cond %1 load i32, i32* %i, align 4 %arrayidx getelementptr inbounds i32, i32* %arr, i32 %1 %2 load i32, i32* %arrayidx, align 4 %3 load i32, i32* %sum, align 4 %add add nsw i32 %3, %2 store i32 %add, i32* %sum, align 4 br label %for.inc for.inc: ; preds %for.body %4 load i32, i32* %i, align 4 %inc add nsw i32 %4, 1 store i32 %inc, i32* %i, align 4 br label %for.cond for.end: ; preds %for.cond %5 load i32, i32* %sum, align 4 ret i32 %5 }注意觀察%sum和%i是顯式alloca分配的局部變量對應(yīng) C 中的int sum 0; int i 0;getelementptr inbounds是 LLVM 對arr[i]的標準翻譯不是簡單加法它包含類型安全檢查inbounds表明不越界icmp slt是有符號整數(shù)比較slt signed less than對應(yīng)i lenbr i1 %cmp, label %for.body, label %for.end是條件跳轉(zhuǎn)%cmp是布爾值寄存器LLVM IR 中沒有“if”關(guān)鍵字只有顯式分支2.3 IR 文件結(jié)構(gòu)解析全局 vs 函數(shù) vs 基本塊三層次嵌套邏輯.ll文件不是扁平文本而是嚴格分層的 SSAStatic Single Assignment結(jié)構(gòu)層級元素作用示例全局層.str private unnamed_addr constant [4 x i8] csum\00全局常量、字符串字面量、外部函數(shù)聲明declare i32 printf(i8*, ...)函數(shù)層define i32 array_sum(i32* %arr, i32 %len) #0 { ... }函數(shù)簽名、參數(shù)、返回值、屬性#0指向attributes #0 { noinline nounwind uwtable }#0屬性控制內(nèi)聯(lián)、異常、調(diào)試信息基本塊層entry:,for.cond:,for.body:控制流單元以標簽開頭以 terminator 指令br,ret,call結(jié)尾每個基本塊內(nèi)指令按數(shù)據(jù)依賴順序排列無 goto關(guān)鍵規(guī)則所有寄存器%sum,%0,%arrayidx都是 SSA 形式每個變量只賦值一次后續(xù)使用即引用該定義getelementptr不計算地址只生成指針類型表達式實際內(nèi)存訪問由load/store完成nswno signed wrap表示該加法不會溢出是 LLVM 優(yōu)化的重要前提若去掉可能觸發(fā) undefined behavior。3. IR 修改與重執(zhí)行手動改.ll文件驗證編譯器行為是否可控3.1 最小改動實驗把add nsw改成add觸發(fā)lli運行時崩潰IR 不是只讀文檔它是可執(zhí)行代碼。llvm-ir-dimostrazione提供run_with_lli.sh腳本用lliLLVM 解釋器直接運行.ll文件lli example.ll # 輸出sum15假設(shè)輸入數(shù)組為{1,2,3,4,5}現(xiàn)在打開example.ll定位array_sum函數(shù)中for.body塊內(nèi)的add nsw指令%add add nsw i32 %3, %2將其改為%add add i32 %3, %2再次執(zhí)行l(wèi)li example.ll # 報錯LLVM ERROR: Code generator does not support arbitrary precision integers yet現(xiàn)象lli崩潰而非靜默錯誤。原因nsw屬性告訴 LLVM “此加法不會溢出”lli依賴該假設(shè)做快速路徑優(yōu)化移除后LLVM 認為可能溢出需啟用大整數(shù)支持APInt但lli默認未啟用。解決要么保留nsw要么用llc編譯成目標碼再執(zhí)行l(wèi)lc -filetypeobj example.ll clang example.o -o example ./example或添加-enable-unsafe-fp-math不推薦。3.2 語義等價替換用phi指令替代load/store實現(xiàn)循環(huán)變量LLVM IR 支持 Phi 指令實現(xiàn) SSA 形式的循環(huán)變量。原array_sum中%i和%sum通過load/store更新我們可重寫為 Phi 版本需手動編輯.ll; 替換 entry 塊末尾的 br label %for.cond ; 為 entry: br label %for.cond for.cond: %i.phi phi i32 [ 0, %entry ], [ %inc, %for.inc ] %sum.phi phi i32 [ 0, %entry ], [ %add, %for.inc ] %cmp icmp slt i32 %i.phi, %len br i1 %cmp, label %for.body, label %for.end for.body: %arrayidx getelementptr inbounds i32, i32* %arr, i32 %i.phi %val load i32, i32* %arrayidx, align 4 %add add nsw i32 %sum.phi, %val br label %for.inc for.inc: %inc add nsw i32 %i.phi, 1 br label %for.cond注意Phi 指令必須放在基本塊開頭且每個入邊%entry和%for.inc必須提供對應(yīng)值%i.phi在%entry入邊為0在%for.inc入邊為%inc形成循環(huán)鏈。驗證保存后lli example.ll仍輸出相同結(jié)果證明兩種 IR 語義等價。這是理解 LLVM 循環(huán)優(yōu)化如 LoopVectorizer的基礎(chǔ)——優(yōu)化器正是將load/store版本識別為可向量化模式再轉(zhuǎn)為phi形式做變換。3.3 外部函數(shù)注入在 IR 中調(diào)用printf并打印中間值想在for.body中插入printf(i%d, val%d\n, i, arr[i]);不能直接寫 C 代碼需在 IR 中聲明并調(diào)用在全局層添加printf聲明declare i32 printf(i8*, ...) local_unnamed_addr .fmt private unnamed_addr constant [18 x i8] ci%d, val%d\0A\00在for.body塊末尾插入%fmt_ptr bitcast [18 x i8]* .fmt to i8* %0 load i32, i32* %i.phi, align 4 ; 若用 phi 版本此處需 load %i.phi %1 load i32, i32* %arrayidx, align 4 call i32 (i8*, ...) printf(i8* %fmt_ptr, i32 %0, i32 %1)重新lli example.ll終端將逐行輸出循環(huán)狀態(tài)。提示bitcast是類型轉(zhuǎn)換指令將字符串常量地址轉(zhuǎn)為i8*printf參數(shù)必須嚴格匹配簽名否則lli會 segfaultlocal_unnamed_addr屬性避免鏈接時符號沖突。4. 常見問題排查IR 生成與執(zhí)行中的五個血淚坑4.1 現(xiàn)象clang -S -emit-llvm報錯error: unable to load plugin libLLVMHello.so原因系統(tǒng)安裝了多個 LLVM 版本如同時有l(wèi)lvm-14和llvm-16clang默認鏈接舊版libLLVM.so但插件路徑指向新版目錄版本不匹配。解決用clang --version確認 clang 版本再執(zhí)行l(wèi)lvm-config --version檢查 LLVM 版本二者必須一致Ubuntu 下卸載舊版sudo apt remove llvm-14*只留llvm-16及配套clang-16。4.2 現(xiàn)象lli example.ll報錯LLVM ERROR: Program used external function malloc原因IR 中調(diào)用了未聲明的外部函數(shù)如new操作符隱式調(diào)用malloclli無法解析符號。解決在.ll文件頂部添加聲明declare i8* malloc(i64)并在運行時鏈接 libclli --stdliblibc example.ll需 LLVM ≥15更穩(wěn)妥做法是避免在演示代碼中使用動態(tài)分配改用棧數(shù)組。4.3 現(xiàn)象修改.ll后lli報錯Invalid redefinition of function array_sum原因編輯.ll時誤刪了函數(shù)末尾的}或復(fù)制粘貼導(dǎo)致多了一個define塊LLVM 解析器認為同一函數(shù)被定義兩次。解決用llvm-as example.ll -o /dev/null 21llvm-as是 IR 匯編器先驗證語法錯誤行號會精確指出缺失}的位置或用 VS Code 安裝llvm插件實時高亮語法錯誤。4.4 現(xiàn)象opt -O2 example.ll -o opt.ll后lli opt.ll結(jié)果與原版不同原因-O2啟用LoopVectorize將array_sum向量化為 4 通道 SIMD 指令但lli對某些向量化指令支持不全尤其 AVX512導(dǎo)致計算錯誤。解決用opt -passesloop-vectorize單獨測試向量化或改用llc -marchx86-64 -filetypeobj opt.ll clang opt.o -o opt ./opt繞過lli解釋器限制。4.5 現(xiàn)象llc example.ll -o example.s生成的匯編中出現(xiàn)movq %rdi, %rax但源碼無long long類型原因LLVM 默認將int參數(shù)32 位提升為 64 位寄存器傳參x86-64 ABI 規(guī)定%rdi是第一個整數(shù)參數(shù)寄存器%rax是返回值寄存器這是 ABI 合規(guī)行為非 bug。解決無需修復(fù)若需驗證 32 位行為加-marchi686強制生成 32 位代碼llc -marchi686 example.ll -o example_32.s。5. IR 優(yōu)化鏈實戰(zhàn)用opt做常量傳播與死代碼消除對比前后差異5.1 用opt做-constprop讓編譯器“算出答案”而非運行時計算llvm-ir-dimostrazione包含constant_propagation.cpp其中定義int const_prop_demo() { int a 42; int b a * 2; int c b 10; return c; }原始 IRclang -O0 -S -emit-llvmdefine i32 const_prop_demo() #0 { entry: %a alloca i32, align 4 %b alloca i32, align 4 %c alloca i32, align 4 store i32 42, i32* %a, align 4 %0 load i32, i32* %a, align 4 %mul mul nsw i32 %0, 2 store i32 %mul, i32* %b, align 4 %1 load i32, i32* %b, align 4 %add add nsw i32 %1, 10 store i32 %add, i32* %c, align 4 %2 load i32, i32* %c, align 4 ret i32 %2 }執(zhí)行常量傳播opt -constprop constant_propagation.ll -o constprop_opt.ll輸出 IR 關(guān)鍵變化define i32 const_prop_demo() #0 { entry: ret i32 94 ; 直接返回 42*210 94 }opt -constprop不僅傳播常量還消除了所有alloca/store/load指令——因為變量值完全已知無需內(nèi)存分配。這是-O1及以上優(yōu)化的基礎(chǔ)步驟。5.2 死代碼消除DCE刪除未使用的printf調(diào)用在example.ll中添加一個未被調(diào)用的printf聲明和調(diào)用declare i32 printf(i8*, ...) .dead_fmt private unnamed_addr constant [10 x i8] cdead\0A\00 ; 在 entry 塊末尾插入 %dead_ptr bitcast [10 x i8]* .dead_fmt to i8* call i32 (i8*, ...) printf(i8* %dead_ptr)此時lli example.ll仍正常運行但printf調(diào)用無實際效果。執(zhí)行 DCEopt -dce example.ll -o dce_opt.ll檢查dce_opt.ll.dead_fmt字符串、printf聲明、bitcast和call指令全部消失——opt -dce從根函數(shù)入口開始追蹤所有可達指令未被任何ret或store依賴的指令即為死代碼。5.3 自定義優(yōu)化流水線組合mem2regsimplifycfginstcombine單個optpass 效果有限真實優(yōu)化是流水線協(xié)作。對array_sum.ll執(zhí)行opt -mem2reg -simplifycfg -instcombine array_sum.ll -o pipeline.ll各 pass 作用mem2reg將alloca/load/store轉(zhuǎn)為 SSA 寄存器即上文phi版本的自動化實現(xiàn)simplifycfg合并冗余基本塊如連續(xù)br label %next可折疊instcombine合并相鄰指令如add i32 %x, 0→%xmul i32 %y, 1→%y。最終pipeline.ll中array_sum函數(shù)體指令數(shù)減少 30%且phi指令自動插入為后續(xù)向量化鋪平道路。提示opt -passesmem2reg,simplifycfg,instcombine是新式寫法LLVM ≥14兼容性更好舊版opt -mem2reg -simplifycfg順序執(zhí)行但instcombine必須在mem2reg后否則無法優(yōu)化load/store模式。6. 從 IR 反推源碼用llvm-dis和grep定位優(yōu)化失效點6.1 當-O2未優(yōu)化時用llvm-dis反匯編.bc文件看真相有時clang -O2 -c example.cpp -o example.o生成的目標文件未達預(yù)期性能懷疑優(yōu)化未生效。不要猜直接看 IRclang -O2 -c -emit-llvm example.cpp -o example.bc llvm-dis example.bc -o example.ll # 將 bitcode 反匯編為可讀 .ll然后搜索關(guān)鍵函數(shù)grep -A 20 define i32 array_sum example.ll若發(fā)現(xiàn)仍有大量load/store無vector相關(guān)指令說明循環(huán)未被向量化。此時檢查數(shù)組長度len是否為運行時變量-O2不向量化變長循環(huán)arr[i]是否有別名如int* arr未加restrictLLVM 不敢假設(shè)無別名是否存在函數(shù)調(diào)用如printf打斷向量化流水線。6.2 用opt -print-after-all捕獲優(yōu)化器決策日志想確認LoopVectorize是否被觸發(fā)加-print-after-allopt -O2 -print-after-all array_sum.ll 21 | grep -A 5 -B 5 LoopVectorize輸出類似*** IR Dump After Loop Vectorization *** define i32 array_sum(i32* %arr, i32 %len) #0 { entry: %wide.load load 4 x i32, 4 x i32* %arr.vec, align 16 ... }%wide.load表示向量化加載一次讀 4 個 int證明LoopVectorize成功。若無此行則優(yōu)化被跳過需檢查-mcpunative或-ffast-math等前置條件。6.3 實戰(zhàn)技巧用llvm-cov關(guān)聯(lián) IR 行號與源碼行號llvm-ir-dimostrazione的CMakeLists.txt啟用調(diào)試信息target_compile_options(example PRIVATE -g -O0)編譯后example.bc包含 DWARF 行號映射。用llvm-cov show查看llvm-cov show example.bc -instr-profiledefault.profdata \ -Xdemanglercfilt -show-inlines輸出中每行 IR 旁標注example.cpp:12:5精準定位arr[i]對應(yīng)哪條 IR 指令。這是調(diào)試優(yōu)化失效的終極手段——當opt日志說“未向量化”你立刻知道是第 12 行的arr[i]訪問觸發(fā)了別名分析失敗。從那以后我每次遇到-O2性能不如-O1都強制走一遍clang -O2 -emit-llvm→llvm-dis→grep流程而不是盲目加-funroll-loops或-marchnative。IR 是編譯器的良心它從不說謊只看你敢不敢直視。希望幫到你。本文還有配套的精品資源點擊獲取