ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

llvm-project实战指南:源码构建、llvmpipe协作与自定义pass开发

2026/9/20 5:05:24 拓冰建站 浏览量
llvm-project实战指南:源码构建、llvmpipe协作与自定义pass开发 拿到“llvm-project”这个标题我第一反应是又一位勇士要跳进编译器的深坑了。说它是“项目”其实它是一整套编译器基础设施的巨型仓库Clang、LLD、libc、compiler-rt、MLIR、Flang、OpenMP运行时全都在里面。2023年我盯着llvm 15.0.7那个版本的tag研究过一阵子当时Mesa里的llvmpipe软件渲染器正好依赖它——也就是你搜到的那条“llvmpipe (llvm 15.0.7, 256 bits)”的来源。这个仓库能帮你做什么往小了说你想自己改一条编译报错信息都得从这儿重编clang往大了说你可以基于MLIR做一套领域专用编译器或者用LLVM的JIT给图形学项目做运行时编译。它适合任何想深入理解编译原理、想做编译器二次开发、或者想给GPU/CPU写底层工具的工程师。这篇博文我就按自己实际折腾llvm-project的经验来写先拆仓库结构再讲怎么从源码构建然后拿llvmpipe举例说明下游项目怎么和LLVM协作最后分享一个手写pass的入门路径。怎么取舍版本、怎么避坑都是我自己踩过的希望帮你少走点弯路。1. 拆开llvm-project这个巨型仓库1.1 一个仓库里到底装了什么第一次git clone完llvm-project你会看到十几个顶层目录。很多人以为LLVM就是“一个编译器”其实它是几十个相对独立的子项目被塞进同一个仓库里。核心的“LLVM”本体在llvm/目录下它是一个完整的编译器后端框架包含IR中间表示、优化pass、指令选择器、寄存器分配器、目标代码生成器以及opt、llvm-as、llvm-dis、llc这些命令行工具。clang/是C/C/Objective-C前端它把源码解析成AST再生成LLVM IR交给后端。lld/是链接器现在ELF、Mach-O、COFF都能用它来链速度比GNU ld快不少。libc和libcabi是C标准库实现compiler-rt负责提供各种运行时库和sanitizer工具。MLIR是多级IR框架这几年火得很TensorFlow和PyTorch底层都有它的影子。flang是Fortran前端openmp是OpenMP运行时polly做循环和多面体优化。不同人的“llvm-project”含义不同有些人只需要clang和llvm两个目录有些人要用MLIR还有些人只关心lld的链接性能。但不管用哪个子项目你都得拉整个仓库因为构建系统、测试套件和工具链版本都是联动设计的。1.2 为什么非要用monorepo管理LLVM团队在2020年前后从Subversion迁到了GitHub顺带做了monorepo整合。之前每个子项目是独立版本号、独立仓库彼此用externals方式引用改一个API要同步改好几个仓库发版时对版本更是折磨。现在全部代码放在同一个repo里原子提交成为可能——比如你改了LLVM核心里的一个接口然后必须同一次提交里把clang、MLIR的调用方一起改掉其他人git log看历史一目了然。代价也很明显仓库体量极大。第一次clone带完整历史大概得六七个GB磁盘不足的同学很容易中途失败。我建议用--depth1做浅克隆只拿最近一次提交构建代码完全够用。反正你是要编译它不是要考古它等真需要查某次历史提交时再git fetch --unshallow也不迟。1.3 版本号与分支到底怎么选LLVM的版本策略很简单每半年发一个大版本版本号如15.0.7表示15.x系列的第七个补丁版。如果你想稳定使用选带tag的release版本如果你想尝鲜新功能用main分支也行但必须接受“今天能编过明天可能就编译失败”的现实。tag在GitHub上就是llvmorg-15.0.7这样的名字要注意和llvm-project-release这种分支区分开。比较常见的坑是main分支上的API一直在变比如New PM的参数类型、pass注册方式可能两三个月就调整一次。我自己的习惯是除非专门研究新特性否则一律锁定release tag构建。下游项目比如Mesa里的llvmpipe通常也只在自己支持的LLVM版本范围内测试版本跨度太大就会出现接口不兼容。所以拿到llvm-project第一步不是写代码而是先想清楚你到底要哪个版本。2. 从git clone到第一份可用工具链2.1 机器准备与磁盘规划构建整个LLVM项目对机器是有要求的。我的建议是内存至少16GB磁盘至少留出60GB空闲空间CPU核心数越多越好因为并行编译会吃满所有核。16GB内存在开满-j16时偶尔会被link阶段卡死那时候Swap狂转工位风扇直接起飞。后来我把CMAKE_BUILD_TYPE从Debug换成Release内存压力明显小很多。磁盘这块多说一句构建目录和源码目录最好分开比如源码放~/src/llvm-project构建放~/build/llvm-project-release。因为LLVM的构建产物很乱中间文件也大分开放后你想删除构建目录就rm -rf一把梭不会污染源码。另外检查一下文件系统格式别用FAT32symlink和权限支持不完整坑你没商量。2.2 CMake配置实战一份能用的构建命令LLVM用CMake组织构建推荐用Ninja生成器。下面这份配置是适用于大多数场景的模板cmake -G Ninja -S llvm-project/llvm -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_ENABLE_ASSERTIONSON \ -DLLVM_CCACHE_BUILDON \ -DLLVM_PARALLEL_LINK_JOBS2这里逐个解释LLVM_TARGETS_TO_BUILD只写X86可以大幅减少编译量你不需要AArch64、RISCV、AMDGPU后端就坚决不编译它们。LLVM_ENABLE_PROJECTS选择要额外构建的顶级子项目这里选了clang和lld。LLVM_ENABLE_ASSERTIONSON在Release模式下也启用断言调试自己的pass时非常有用但性能会略下降发布版工具链可以关掉。LLVM_CCACHE_BUILDON启用ccache缓存增量编译体验天翻地覆。LLVM_PARALLEL_LINK_JOBS2限制链接并行度防止link阶段内存爆掉。配置完成后直接ninja -j16。第一次全量构建根据机器配置可能耗时20到60分钟多核机器快一些笔记本可能要熬一会。2.3 从构建类型到cache影响构建体验的细节很多人上来直接-DCMAKE_BUILD_TYPEDebug然后被Link速度折磨到怀疑人生。Debug模式代码零优化符号信息全适合断点调试但生成的二进制大、链接慢、运行也慢。我的建议是日常开发用Release加LLVM_ENABLE_ASSERTIONSON既有断言保护又有接近真实场景的性能调试大多数逻辑问题都够了。真到了需要逐行单步的时候再单独编一份带符号的Debug版本。ccache这里多说一句它缓存的是编译器的预处理输出和编译结果命中后就是“复制”而不是重新编译。LLVM这种几万个源文件的项目ccache的命中率非常可观。我试过一次改动一个头文件没有ccache要重编几千个文件有了ccache只重编真正依赖它的那部分速度快了至少十倍。前提是你得装好ccache并让CMake自己找得到它否则直接-DLLVM_CCACHE_BUILDON会报错找不到编译器。构建中还有一个容易忽略的点LLVM_ENABLE_PROJECTS和LLVM_ENABLE_RUNTIMES不要混用。像libc、compiler-rt这类运行库组件新版LLVM官方建议放进LLVM_ENABLE_RUNTIMES因为它们需要先构建一份目标编译器再用这个编译器去编译这些运行库。顶级的“项目”则直接随主工具链一起被编译。搞错了就会出现奇怪的循环依赖报错。3. llvmpipe一个真实的LLVM落地方案3.1 llvmpipe在干什么搜到“llvmpipe (llvm 15.0.7, 256 bits)”时你大概率是在看Mesa或图形驱动的日志。llvmpipe是Mesa提供的一个纯软件渲染器它利用LLVM做JIT编译把GLSL或SPIR-V着色器先转成LLVM IR再趁热编译成宿主CPU的机器码然后CPU跑这些机器码来完成光栅化、片段着色、深度测试等工作。这个方案的意义在于没有GPU的虚拟机、云服务器、或者显卡驱动出了问题的机器上你还能靠llvmpipe跑起来OpenGL或Vulkan应用虽然慢但功能是完整的。它同时也是一块绝佳的实验田——如果你想研究如何把高级语言编译到高性能机器码llvmpipe就是活生生的教材。从构建角度说Mesa在编译时会检测系统中的LLVM版本再用对应的CMake配置去链接。如果系统里常有多个LLVM版本共存Mesa很可能会链接到错误版本你看到的就是编译失败或者运行时崩溃。这种“下游项目链接LLVM”的坑我后面单独讲。3.2 256 bits、AVX2和向量化宽度日志里的“256 bits”指的是llvmpipe使用的向量位宽。现代CPU的SIMD指令集AVX2把向量寄存器扩展到256位也就是一个寄存器能同时放8个float或4个double。llvmpipe在JIT编译着色器时LLVM后端会根据CPU特性选择是否启用AVX2并把IR里的向量操作映射到YMM寄存器上。如果你在日志里看到256 bits说明LLVM认为当前CPU支持AVX2并且正在用255位宽的向量来生成代码。这里的“256 bits”不是llvmpipe自己做出来的而是LLVM后端的向量化与寄存器分配结果。用户写GLSL时用的是vec4、vec8这样的抽象向量类型LLVM在中间表示里把这些向量运算展开为平台相关的SIMD指令序列。所以llvmpipe性能受两方面影响一是宿主CPU支持的SIMD指令集二是LLVM代码生成的质量。换个CPU或者换一个LLVM版本可能同一段着色器的机器码就变了。注意真正让代码跑得快的不是LLVM版本号有多新而是它为你目标CPU生成的指令序列是否踩准了SIMD特性。这就是为什么很多渲染库只测试特定几个LLVM版本不敢随便升。3.3 下游项目与LLVM版本打交道时容易踩的坑下游项目链接LLVM最大的问题就是版本漂移。Mesa、Julia、Rust、Swift这些项目都会声明自己支持的LLVM版本范围但不代表你系统里装的就是那个版本。常见报错是undefined reference to llvm::Something::Something()一看就是头文件版本与库版本不一致接口对不上。我的排查路径是先用llvm-config --version查系统默认版本再用find /usr -name libLLVM*.so*看看有哪些库文件。如果Mesa之类的项目用了find_package(LLVM)它会读LLVM的CMake配置那个配置里记录的版本若是与你期望的不一致就得显式给CMake传-DLLVM_DIR/path/to/llvm/lib/cmake/llvm。这种问题不是LLVM本身的bug而是环境管理不到位。还有一类坑是“同一个进程加载了多个LLVM运行时”。比如一个Python扩展内部用LLVM另一个共享库也自带LLVM两个版本的全局符号冲突后直接崩溃。解决办法通常是加隐藏符号可见性或者把每个组件静态链接各自版本的LLVM。这在调试时非常烧脑一眼看去是空指针实则符号表碰撞。4. 开发者的第一课自己写一个LLVM pass4.1 先看懂IR再动手想在llvm-project上做开发第一个要掌握的就是LLVM IR。IR是LLVM的核心中间表示它介于源码和目标机器码之间具有类似汇编的形态但携带类型信息和控制流图结构。一个简单的加法函数在IR里长这样define i32 add(i32 %a, i32 %b) { %sum add i32 %a, %b ret i32 %sum }建议先用clang -S -emit-llvm把C文件编成.ll文件再用opt去跑现成的分析和转换pass比如opt -passesinstcount统计指令数用opt -passesmem2reg做提升。等你能读懂IR里basic block、phi、load/store这些概念之后再动手写pass会顺手很多。4.2 新Pass Manager框架下的pass骨架现在不建议学旧的legacy pass managerLLVM 15后的默认框架是New Pass ManagerNPM。写一个最简的分析或转换pass头文件里继承相应的基类。下面是一段把add替换成sub的示例性转换pass骨架注意它只展示了代码组织方式不追求正确的IR合法性检查#include llvm/IR/Function.h #include llvm/IR/Instructions.h #include llvm/IR/IRBuilder.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h using namespace llvm; namespace { struct SillyAddPass : public PassInfoMixinSillyAddPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { bool Changed false; for (auto BB : F) { for (auto I : BB) { if (auto *BinOp dyn_castBinaryOperator(I)) { if (BinOp-getOpcode() Instruction::Add) { // 将 add 指令修改为 sub 指令 BinOp-setOpcode(Instruction::Sub); Changed true; } } } } return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); } }; } // end anonymous namespace extern C ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, SillyAddPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) - bool { if (Name silly-add) { FPM.addPass(SillyAddPass{}); return true; } return false; }); }}; }这是标准的pass插件写法定义pass类导出llvmGetPassPluginInfo然后在回调里注册到PassBuilder。编译时用clang编译成动态库运行时用opt -load-pass-plugin加载。4.3 用opt跑通pass的调试流程写完pass后把插件编译成.so文件再用opt命令验证opt -load-pass-plugin build/yourpass.so -passessilly-add -S test.ll -o out.lltest.ll是你要处理的IR文件-S表示输出人类可读的IR文本out.ll是处理后的结果。如果一切正常原来的add指令会变成sub。这个过程非常痛快不需要链接进clang不需要完整工具链构建改完pass一重新编译动态库再跑opt就行迭代速度非常快。调试时最常用的手段是dbgs()输出它在llvm/Support/Debug.h里可以通过-debug-onlypass名控制开关。比printf好在它完全受LLVM_DEBUG宏控制调试完了不用删发版时自动去掉。另一个实用技巧是F.viewCFG()它会用Graphviz生成当前函数的控制流图可视化分析循环结构和分支跳转找问题能省一半时间。提示自己写pass时最容易翻车的是IR合法性。比如你把add改成了sub但两者类型和操作数位宽必须一致否则后端生成机器码时会崩溃。先在小函数上试再扩大到真实场景。5. 使用llvm-project常见问题速查5.1 构建与依赖类问题LLVM构建失败多数出在环境问题而不是代码问题上。我汇总过几个高频报错报错特征常见原因解决方案CMake Error: The source directory does not exist-S路径指向了llvm-project而不是llvm子目录源码路径必须带/llvmninja: error: loading build.ninja: No such file忘记先执行cmake配置先cmake配置生成Ninja构建文件内存不足link阶段被OOM KillDebug模式或并行链接任务过多改用Release调低LLVM_PARALLEL_LINK_JOBSCould NOT find ZLIB之类依赖缺失缺少系统开发包安装对应dev包比如zlib1g-dev编译速度极慢没有开ccache或目标架构太多开LLVM_CCACHE_BUILD精简LLVM_TARGETS_TO_BUILD最让人头疼的其实是“存量构建突然坏了”。通常发生在你更新了源码分支或者切换了CMake配置。这时候别硬着头皮ninja尝试删除build目录重新配置大部分问题会自己消失。5.2 运行与调试类问题工具链构建成功后运行阶段仍有不少坑。opt报unknown pass name是因为你写的pass没注册成功常见于llvmGetPassPluginInfo里拼接的pass名不一致或者写成了legacy pass manager接口。clang内部报Unable to load plugin则是插件ABI不兼容九成是插件对应的LLVM头文件版本和当前运行工具链的版本不一致重新用同一个llvm-project构建插件即可。还有一个高频问题调试Release版工具链时栈回溯全是unknown符号这时候要重新编一个带调试信息的版本或者在CMake里打开LLVM_BUILD_LLVM_DYLIB编译出共享库这样外部工具能通过libLLVM.so统一加载不同的组件符号解析会更容易跟踪。5.3 我私藏的排查技巧排查LLVM问题时我有一套固定的顺序。第一步看版本clang --version、opt --version确认工具链来自同一个构建目录。第二步看链接用ldd cminja列出clang链接到的LLVM库路径很多时候它会链到系统自带的/usr/lib/libLLVM.so而不是你刚编出来的版本这样行为就诡异了。第三步才看报错堆栈。还有一个很少人注意的技巧LLVM自身命令行工具几乎都带-debug-only参数你把-debug-onlyir或者-debug-onlyisel传进去它会输出大量内部决策信息。虽然刷屏很狂但排查pass顺序和代码生成问题时这些日志比断点调试还管用。最后说几句心里话从第一次对着llvm-project的构建日志发呆到现在能熟练地改pass、查issue我在这个项目上花的时间还真不少。llvm-project最典型的“坑”并不是代码有多深奥而是它更新太快、牵涉面太广你以为只是改个API结果下游十几个项目都要跟着适配。所以我个人的体会是不要一上来就吞掉所有子项目选定一条主线深入进去比如先玩clang的前端或者先玩opt的pass跑通了再横向扩展。llvmpipe和LLVM 15.0.7就是很好的起点——版本不新不旧文档齐全社区里踩过坑的人也多。真遇到问题时愿意花时间分解报错、读IR输出比到处复制粘贴别人的配置更管用。后续如果你想深挖还可以把MLIR、Flang、或者LLVM的JIT接口一个个拆开来看每个都是一座金矿。只要你肯折腾llvm-project值得你翻来覆去吃透它。