尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

LLVM Project深度解析:从IR到Pass优化的编译器实战

发布时间:2026/9/20 11:17:55

资讯中心
01
ARTICLE

LLVM Project深度解析:从IR到Pass优化的编译器实战

LLVM Project深度解析:从IR到Pass优化的编译器实战
1. llvm-project是什么为什么它值得你花时间研究这几年身边做编译器、做编程语言、做静态分析的朋友几乎人手一个llvm-project的本地clone。如果你关注过Rust、Swift、Zig这类新语言会发现它们的底层工具链都跑在LLVM之上。llvm-project这个仓库就是LLVM生态的总集散地官方用monorepo方式把LLVM核心、Clang、LLD、libc、MLIR等一系列子项目放在同一个版本体系里维护。换句话说想搞懂现代编译器是怎么工作的或者想做出一个属于自己的语言前端llvm-project就是你绕不开的那座山。我第一次接触llvm-project是在研究一个自定义DSL的编译问题时当时只是需要把一段领域专用代码翻译成中间表示结果越挖越深发现整个项目简直就是一座金矿。它解决的不只是“把C代码变成机器码”这么简单的问题而是把编译器前端、优化器、后端全部拆成了可独立使用的模块你完全可以只拿它做中间表示转换或者只拿它的优化管线来加速已有的代码再或者拿clang-tidy做代码风格检查。它的适用人群也特别广语言设计者可以用它快速搭出前端嵌入式工程师可以用它做交叉编译和指令集适配性能优化工程师可以用它分析优化报告高校研究者在它上面做实验发论文。这篇博文我想从整体架构、核心子项目、真实构建过程、常见坑四个角度来梳理一遍既说清楚原理层面的“为什么”也把我在实际操作中踩过的坑、试过的参数、验证过的构建命令一并分享出来希望能帮准备入坑的朋友节省时间和精力尤其是那些刚打开llvm-project仓库却不知道从哪下手的人。2. 整体架构拆解一个大型编译基础设施的设计逻辑2.1 monorepo的版本一致性策略llvm-project采用monorepo模式也就是把LLVM核心、Clang、LLD、libc、compiler-rt、polly、MLIR、flang等全部放在同一个Git仓库中管理。我第一次看到这个仓库的目录结构时第一反应是“这也太杂了”但仔细研究后才发现这种组织方式背后有非常现实的理由。最早的LLVM是多个独立仓库llvm、clang、lld、libc各自拥有自己的版本号、issue列表和release节奏。结果每次发版本维护者都要花大量精力协调这些仓库之间的兼容性比如Clang 12需要依赖LLVM哪一个commit格式上稍有偏差整套工具链一起构建时就会出现一堆诡异问题。改成monorepo之后只要切到某个release分支比如llvmorg-19.1.0所有子项目自动对齐到同一个commit构建和调试的心智负担小了很多。从贡献者角度看一个pr如果修改了LLVM核心和Clang的接口在monorepo下可以直接放在同一个PR里提交代码评审和历史回溯都很方便。从使用者角度看clone一次就能拿到完整的工具链源码配合CMake的一个命令就能把CLang、LLD这些子项目全部编出来。这种协调版本一致性的方式是llvm-project能够支撑如此庞大生态的基础设施级决策。2.2 三段式编译器架构前端、中端与后端的解耦LLVM最核心的设计思想是把编译器分为前端Frontend、中端Optimizer、后端Backend三段。前端负责把源代码解析成中间表示中端在这个中间表示上做跨语言、跨平台的优化后端再把它翻译成特定的目标机器码。这个思想并不是LLVM首创但LLVM把它做到了极致并且一切围绕着一个统一的中间表示也就是LLVM IR。我用一个生活化类比来解释这个架构前端相当于把中文、英文、日文三种原稿都翻译成同一种“标准格式文档”中端就像对这个标准格式文档做内容润色、删减冗余、调整结构后端则是按照不同出版社的排版要求把同一份标准文档排成简体版、繁体版、英文版。这样一来新增一种编程语言只需要写一个新的前端新增一种CPU架构只需要写一个新的后端前端和后端互不干扰工程量大幅缩减。这种解耦带来的直接好处是你不必为了支持一种新语言就从头到尾实现一套完整的编译器。语言前端只需要把AST生成LLVM IR剩下的优化、寄存器分配、指令选择等重活都由LLVM完成。反过来想支持一种新CPU架构也只需要把LLVM IR翻译成这种架构的指令即可所有语言的代码就都自动能在这种新架构上运行了。2.3 核心子项目各司其职llvm-project仓库里躺着十几个子项目初次看到容易眼花缭乱。我来梳理其中最关键的几个帮你建立一个“地图”。LLVM核心库是所有子项目的地基它实现了IR的数据结构、Pass优化管线、目标描述TD框架、JIT引擎如ORC等基础能力。Clang是C/C/Objective-C的前端也是社区最常用的编译器入口它的代码质量和诊断信息在开源编译器中属于顶级水准。LLD是链接器以速度快、内存占用低著称在大型C项目的链接场景下比系统默认GNU ld能快出好几倍。libc是C标准库实现配合Clang使用体验很流畅。compiler-rt提供了各种运行时支持比如sanitizer系列AddressSanitizer、UBSanitizer还有内置整数函数在多平台下的实现。MLIR是多级中间表示框架专门解决Domain Specific语言和异构硬件加速的问题这几年在AI编译、芯片编译器领域特别火。Flang是对Fortran的实现polly是面向循环优化的多面体模型优化器clang-tools-extra则包含clang-tidy、clangd等实用工具。我最初只想要一个能用的clang后来慢慢把LLD、compiler-rt、MLIR都编了进来发现它们的配合深度远超我最初的预期。比如用clang编译C代码调用LLD做链接再开启AddressSanitizer做内存检测整套流程在llvm-project的体系里完全闭环这比单独下载二进制发行版要灵活得多。3. 核心细节解析IR、Pass优化管线与后端代码生成3.1 LLVM IR的独特性SSA与强类型设计LLVM IR是这个项目的灵魂理解它才算真正踏入LLVM的世界。IR是一种静态单赋值SSA形式的强类型语言每条指令都对应一个版本化的变量每个变量只能被赋值一次。这种设计让数据依赖关系变得极其清晰优化器可以轻易判断哪些计算是冗余的、哪些变量在后续没有被使用。举个例子下面这段LLVM IR实现了一个简单的加法函数define i32 add(i32 %a, i32 %b) { %1 add i32 %a, %b ret i32 %1 }这里i32是32位整数类型%a和%b是函数的两个参数%1是把两个参数相加的结果。可以看到每条指令都有明确的类型信息强类型的好处是可以直接在IR层面做类型相关优化而不必关心源语言。LLVM IR有三种表现形式内存中的数据结构、可读的文本文件.ll、可序列化的位码文件.bc。三种形式等价可以互相转换。我通常用命令clang -S -emit-llvm foo.c -o foo.ll查看C代码对应的IR这是学习IR的一个非常直观的入口。3.2 Pass优化管线优化器如何工作IR被生成之后会经过一长串Pass的加工。Pass就是执行某种特定优化的代码模块比如Dead Code Elimination消除死代码、Loop Unroll循环展开、Inlining内联、GVN全局值编号等。这些Pass像流水线上的工位每个工位只做一件事又互相协作。LLVM的Pass框架经历过一次大的演进老派Legacy Pass Manager逐步被New Pass Manager新PM取代。在新PM中我用得比较多的形式是基于PassInfoMixin写一个自定义FunctionPass比如#include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/IR/LegacyPassManager.h #include llvm/Pass.h #include llvm/Transforms/IPO/PassManagerBuilder.h using namespace llvm; namespace { struct MyPass : public PassInfoMixinMyPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { // 这就是每个函数会被执行的核心逻辑 for (BasicBlock BB : F) { for (Instruction I : BB) { // 对每条指令做分析或变换 errs() I \n; } } return PreservedAnalyses::all(); } }; } // 新PM的注册方式 llvm::PassPluginLibraryInfo getMyPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, MyPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name my-pass) { FPM.addPass(MyPass()); return true; } return false; }); }}; } extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getMyPassPluginInfo(); }写成插件后可以用clang -fpass-pluginbuild/MyPass.so运行在任意C代码上。我自己的经验是刚开始写Pass时不要一上来就做激进变换先把IR打印出来、把访问到每个函数的信息log出来明白Pass的遍历模型之后再逐步添加实际修改逻辑。3.3 后端代码生成从IR到机器码的旅程优化后的IR不会直接变成机器码它还要经过后端的多层处理。在LLVM后端任何一个目标架构如X86、AArch64、RISC-V都对应一个TableGen描述的指令集文件.td。后端流程大致是IR先被Legalize合法化把不支持的指令类型或操作转换/拆解成目标支持的形式然后通过SelectionDAG选择DAG做指令选择把IR指令映射成目标机器的真实指令接着是寄存器分配把无限虚拟寄存器分配到有限物理寄存器上寄存器溢出时插入加载/存储指令再是指令调度按目标CPU的流水线特性重排指令顺序最后输出汇编代码或直接生成机器码。这个流程的理解难点在于指令选择阶段LLVM zig编译器一般会把IR变成SelectionDAG节点然后与目标架构的td模式做匹配。写目标后端的人大部分时间在写TableGen文件通过定义模式来告诉DAG匹配器“IR里这样一段组合模式可以折叠成那条特定指令”。比如AArch64后端里有大量带条件执行的指令这些折叠规则都写在td文件里。对大多数使用者来说不需要深入后端实现也能用好LLVM但明白这个流程你在看llc -O2 -marchxxx的汇编输出时就会恶心到明明IR很简单怎么汇编却那么绕其实这背后就是指令选择、寄存器分配和调度的共同结果。4. 实操过程从clone到产出可用的clang与LLD4.1 环境准备与依赖检查在开始构建之前先列出机器的最低配置内存16GB及以上、磁盘可用空间120GB以上、支持C17的编译器至少GCC 7.1或Clang 5.0以上、CMake 3.20及以上、Python 3.6及以上以及Ninja构建工具。我实际使用的是32GB内存的机器构建时间能控制在1小时以内如果你的内存只有8GB也不要放弃减少并行度-j2也能跑只是要多等一阵。我用的是Ubuntu 22.04安装依赖的命令如下sudo apt update sudo apt install -y git cmake ninja-build clang lld python3如果你是macOS可以用Homebrew安装llvm、cmake、ninjaWindows则可以启用Visual Studio的C开发环境并使用LLVM官方提供的构建指南。4.2 获取源码与配置CMake获取llvm-project源码推荐用浅克隆加指定分支的方式git clone --depth 1 --branch llvmorg-19.1.0 https://github.com/llvm/llvm-project.git cd llvm-project这里指定了llvmorg-19.1.0发布版标签好处是版本稳定配套工具的兼容性也经过了验证。如果你想体验最新主分支可以去掉--branch参数但主分支每天都在变构建失败的概率会高不少建议新手从稳定分支开始。关键环节是CMake配置。我把我的常用命令贴出来cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;clang-tools-extra \ -DLLVM_TARGETS_TO_BUILDX86;AArch64 \ -DLLVM_ENABLE_RUNTIMESlibcxx;libcxxabi;compiler-rt这里解释几个重要参数。CMAKE_BUILD_TYPERelease表示构建优化过的发布版运行速度快但编译耗时稍长LLVM_ENABLE_PROJECTS指定了需要一并构建的主项目我选了clang、lld和clang-tools-extra其中clang-tools-extra包含clangd和clang-tidy这两个工具日常很实用LLVM_TARGETS_TO_BUILD限定目标架构如果只写X86构建时间和体积能减少很多LLVM_ENABLE_RUNTIMES则用来指定运行时组件注意在LLVM 18之后libcxx等运行时组件推荐放到LLVM_ENABLE_RUNTIMES而不是LLVM_ENABLE_PROJECTS两者的默认构建行为不同这也是新手最容易踩的坑之一。4.3 执行构建与时间优化配置完成之后开始构建ninja -C build clang lld clangd clang-tidy这里我特意没有直接跑ninja all而是指定了具体目标。如果直接跑all会把所有工具、测试、文档全部编出来耗时和磁盘占用都翻倍。按需构建是一个非常重要的习惯我一开始傻乎乎地跑了两天全量构建后来每次都是只编我需要的组件。构建时间受机器配置影响很大。我用的是一台8核16线程的机器构建上述目标大约花了40分钟。如果你的配置更低建议使用ninja -j2降低并行度防止机器卡死。这里有一条关键经验链接阶段是最吃内存的我有一个朋友用4核8GB内存的机器编译时卡在链接clang阶段直接OOM了解决办法是在CMake配置时加上-DLLVM_PARALLEL_LINK_JOBS2限制同时链接的任务数避免多个大二进制同时链接撑爆内存。构建完成后验证一下成果./build/bin/clang --version能正常输出版本信息说明这整套工具链已经可以使用了。写一个简单的C程序测试#include stdio.h int main() { printf(llvm-project build ok\n); return 0; }编译运行./build/bin/clang test.c -o test ./test输出正常说明最基本的编译-链接-执行流程已经打通。4.4 TableGen、libc与跨平台工具链的补充说明如果要给LLVM贡献新指令或新目标就需要接触TableGen。TableGen是一种描述性语言用来描述指令集、寄存器、调用约定等信息。比如在X86.td里你可以定义一个指令模板def ADD32rr : I0x01, MRMDestReg, (outs GR32:$dst), (ins GR32:$src1, GR32:$src2), add{l} {$src2, $src1|$src1, $src2}, [(set GR32:$dst, (add GR32:$src1, GR32:$src2))];那段[(set ...)]就是告诉LLVM后端“如果IR中出现一个加法操作可以匹配ADD32rr这条指令。”TableGen会在构建时生成C代码供指令选择器使用。刚开始看td文件会很懵建议从简单架构比如RISCV的td文件入手它的指令集更规整更容易理解。再说一下libc。如果你要构建一个纯LLVM工具链在一些Linux环境上编译器运行时默认会链接到系统自带的libstdc除非你在CMake中显式启用libc。当LLVM_ENABLE_RUNTIMES包含libcxx和libcxxabi时构建出的clang可以把程序链接到新的libc上用工具链自带的C标准库而不是系统的这样在交叉编译或隔离环境中会更可控。跨平台工具链方面如果你做嵌入式开发需要配置好sysroot和--target参数。比如在x86的机器上交叉编译AArch64的程序./build/bin/clang --targetaarch64-linux-gnu -marcharmv8-a \ --sysroot/path/to/aarch64-sysroot test.c -o test_aarch64前提是CMake构建LLVM时已经把AArch64后端编进去了这就是LLVM_TARGETS_TO_BUILD里加AArch64的意义。5. 应用场景与影响范围从语言实现到AI编译5.1 新语言快速落地与DSL工具链正因为三段式架构解耦很多新语言可以站在LLVM肩膀上快速实现前端几天之内就能把hello world编译成机器码。Rust、Swift、Zig最初都依托LLVM的IR和代码生成能力实现高效编译Julia虽然有自己的专属实现但优化层面与LLVM也有深层联动。对于我这种做嵌入式DSL的人最典型的路径是设计AST → 生成LLVM IR →可选加载自定义Pass优化 → 交给LLVM后端生成目标代码。这里的核心工作量集中在AST到IR的翻译逻辑优化和后端就交给LLVM。当然“站在肩膀上”并不意味着没有代价。你需要学习LLVM的C API理解IR的SSA约束还需要对LLVM内部的数据结构比如Module、Function、BasicBlock、Instruction之间的关系有基本掌握。好在官方文档和示例很多stackoverflow上相关话题也异常活跃。5.2 静态分析、代码安全与工程规范clang-tools-extra带的clang-tidy是很多C工程标配的静态检查工具它可以检查内存泄漏风险、代码风格、可疑的C用法等。我自己的一个项目里CI流程中就加了clang-tidy检查每次代码提交后自动跑一遍对工程质量提升非常明显。clang的静态分析器Clang Static Analyzer可以进行路径敏感分析在单元测试之外发现一部分逻辑缺陷。clangd则是IDE里实现智能提示、跳转、重命名、代码补全的关键组件很多编辑器的C体验都建立在它之上。我在VSCode和neovim里都配过clangd整体体验非常流程尤其是在大型C项目里比传统的ctags或仅靠ctags的体验好了太多。5.3 MLIR与AI编译的想象空间MLIR是llvm-project里近年来最火的新贵。它不是替代LLVM IR而是提供了一个多级IR框架最上层接近源语言抽象最底层接近LLVM IR从而让编译流程可以逐层lower。AI编译器如TensorFloat、MLIR-HLO、ONNX-MLIR包括很多的AI芯片编译器都基于MLIR做前端表达式分析和后端指令映射它解决的核心痛点是领域专用硬件NPU、DSP上传统LLVM IR层级太高表达不了特定的算子、数据布局和流水线结构。如果你关注的是AI编译器方向llvm-project里的MLIR会是你花时间最值的一块。但注意MLIR的学习曲线比普通LLVM开发略陡建议先跑通toy教程再逐步接触方言Dialect的定义和转换Conversion管线。5.4 性能工程基于优化报告做热点定位在性能优化领域LLVM的优化日志能力非常强大。-Rpassloop-vectorize可以看到哪些循环被向量化-Rpass-missedloop-vectorize可以看到哪些循环没有被向量化以及原因-Rpass-analysisinline可以看到内联决策和拒绝内联的理由。这些都是我日常性能调优的必备工具比我在陌生代码里猜热点快了太多。另外pgoProfile-Guided Optimization和BOLTBinary Optimization and Layout Tool也都在llvm-project生态里前者基于运行时采样信息反馈给编译器优化分支布局和热路径后者直接对已经发布的二进制做优化布局。它们对性能敏感型的服务端代码带来10%-30%的指令缓存命中率提升都很常见。6. 常见问题与排查技巧实录6.1 构建期常见问题速查表我在社区里见过太多人在构建llvm-project时踩坑下面把最高频的问题整理成一个速查表方便你对照排查。症状常见原因解决方案CMake报找不到Python/CMake版本过低依赖版本不足升级CMake到3.20确保Python 3.6编译到链接阶段进程被杀OOM并行链接任务过多配置-DLLVM_PARALLEL_LINK_JOBS2ninja报错ninja: error: loading build.ninjaCMake配置失败清空build目录重新cmake核心头文件找不到llvm/Config/config.h未执行CMake直接取头文件必须先在build目录完成CMake配置链接时报fatal error: error in backend目标架构未编入检查LLVM_TARGETS_TO_BUILD是否包含目标架构-fpass-plugin插件加载失败Pass插件API版本不匹配确认插件编译时的LLVM版本与clang版本一致其中OOM这问题最容易在低配机器上翻车。你别以为限制-j4就行因为链接是单独的任务即使两个文件同时编译只要两个链接同时开跑4GB内存基本瞬间被打爆。所以必须限制链接并行数编译并行数可以适当高一些。6.2 调试心得断点pass时怎么观察结果在开发pass时最常用的调试方式是加打印。新PM里在持有Function引用时可以用F.dump()打印当前IR在持有Module时用M.dump()。如果你想观察pass执行前后的变化可以在pass入口打印Before在出口打印After再diff两个IR文件。也可以使用LLVM内置的计时和分析工具比如-ftime-report可以查看每个pass的时间占比这里的pass时间包括编译器前端、优化器和代码生成各阶段的耗时优化阶段可能因为某个特定的pass算子特别复杂出现明显的时间尖峰。你看到的任何异常耗时通常都能通过-ftime-report定位到具体的pass或阶段。另一个小技巧如果你修改了LLVM源码之后再重新构建发现行为没有变化先检查一下是不是用了旧版构建的build/bin/clang。我干过好几次这种傻事明明重新编译了却还是在用老二进制。6.3 如何减小构建时间和磁盘占用频繁改pass代码需要反复重建llvm-project全量重编确实肉疼。我目前用的策略有这么几条第一只编需要的组件比如只做pass开发时只编ninja -C build opt第二用ccache缓存编译产物在CMake配置里加-DLLVM_CCACHE_BUILDON第二次构建能节省一半以上时间第三用ReleaseAssert配置也就是-DCMAKE_BUILD_TYPERelWithDebInfo -DLLVM_ENABLE_ASSERTIONSON这个组合既保留了断言检查又带了优化和调试信息适合开发阶段的pass验证。这个版本比纯Debug构建快不少又比纯Release更容易定位异常。磁盘占用方面如果你不需要跑测试可以加-DLLVM_INCLUDE_TESTSOFF不需要文档就加-DLLVM_INCLUDE_DOCSOFF不需要示例和工具就加-DLLVM_INCLUDE_EXAMPLESOFF -DLLVM_INCLUDE_BENCHMARKSOFF。我构建一个最小工具链的时候磁盘占用可以从120GB降到40GB左右效果非常明显。6.4 版本兼容的陷阱llvm-c和C API的稳定性最后说一个很容易踩的版本兼容问题。LLVM的C API变动非常大不同主版本之间的接口往往不兼容。比如早期Pass接口和现在的New Pass Manager接口差别很大你写一个pass时如果用在线教程里的旧代码直接编译很可能报一堆过时或找不到符号的错误。此时优先查看对应版本的官方llvm/examples目录以及header文件里的注释来更新写法。不要照搬网上老旧的教程到最新版本上血的教训。如果只想写pass而不想用LLVM官方源码树也可以独立构建一个pass插件只要include和链接路径指到你的LLVM安装目录即可。但使用C APIllvm-c做动态库交互时版本的二进制兼容性通常比C API好一点只是能力也弱一些。7. 写在最后的一点个人心得从我第一次把clang编出来到今天可以顺手在llvm-project上写插件、看优化报告、做交叉编译回头再看这个项目它给我的最大感受是“庞大但又不失优雅”。庞大在于它是数千万行级别的巨型项目优雅在于它把编译器这个古老而复杂的话题拆解成了清晰的前端—中端—后端框架并且在每个层面都留出了用户自定义和二次开发的口子。如果你打算入坑我建议别一上来就啃源码先拿现成的工具用起来比如用clang -S -emit-llvm观察C代码的IR、用clang-tidy给你的项目做一次巡检、用opt -passes跑几个优化Pass建立感性认识之后再深入源码读数据结构、写一个最小Pass插件。这样循序渐进会顺畅很多。后续还可以进一步向MLIR、BOLT方向继续挖围绕llvm-project能做的事非常多它值得你投入时间。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。