资讯动态

llvm-project全解析:仓库结构、构建、llvmpipe与自定义Pass

发布时间:2026/9/19 8:07:51 来源:尧图企业网站定制
很多人第一次看到llvm-project这个仓库名时会以为它是一个像 GCC 一样的编译器。实际上它是一个 monorepo里面装的是一整套编译器基础设施LLVM 核心库、Clang、lld、libc、mlir、flang还有 Mesa 里那个叫 llvmpipe 的软件渲染器背后依赖的 JIT 引擎。我最初在 15.0.7 上折腾 llvmpipe 时发现版本之间的差异比想象的更值得注意尤其是 256 位向量寄存器路径上的表现。这篇文章就以llvm-project为主线聊聊仓库结构、源码构建、后端向量化、llvmpipe 的运行机制以及怎样快速写一个自定义 Pass。1. llvm-project到底装了什么仓库结构拆解1.1 从编译器到编译器工厂的转变LLVM 的名字来自 Low Level Virtual Machine但它现在和虚拟机关系不大而是一套模块化的编译组件。llvm-project就是一个把所有这些组件放到一个版本化仓库里的 monorepo。你 clone 下来后顶层目录会有llvm、clang、lld、lldb、compiler-rt、mlir、flang、libc、libcxx等。这种结构不是随意堆砌因为各项目之间存在紧密的接口依赖。例如 clang 需要用 LLVM 的 TableGen 生成诊断定义lldb 依赖 clang 的 ASTmlir 可以 lowering 到 LLVM IR。把它们放进同一个仓库、同一个版本、一起测试就避免了依赖地狱。你在 GitHub 上看到的llvm/llvm-project就是这样一个工程。1.2 最常用的几个子项目分工目录作用使用场景llvm/核心库包括 IR、中端优化 pass、后端指令选择、寄存器分配、指令调度、CodeGen、JIT几乎所有项目的基础clang/C/C/Objective-C 前端日常编译 C/Clld/高性能链接器替代系统 ld链接速度更快lldb/调试器基于 LLVM 生态的调试体验compiler-rt/运行时库sanitizers、builtins、profile内存检测、覆盖率收集mlir/多级中间表示框架深度学习编译器、自定义 DSLflang/Fortran 前端科学计算场景libcxx/ libcxxabi/ libunwind/C 标准库及底层运行时配合 clang 使用polly/多面体模型优化循环嵌套深度优化openmp/OpenMP 运行时并行编程其中llvm/lib/Target/X86/这类目录负责具体的 CPU 后段。我经常看到有人只 clone 了llvm-project但不知道从哪个目录开始看其实llvm/才是核心clang/只是前端之一。1.3 monorepo 的版本控制策略采用monorepo的最大好处是原子提交一个改动如果同时涉及 LLVM 核心和 Clang就可以在一个 commit 里完成所有 CI 一起跑。缺点是仓库体积极大--depth 1的浅克隆仍然可能超过 1GB。不过构建时不一定需要全部编译。通过 CMake 的LLVM_ENABLE_PROJECTS选项可以只启用部分子项目。例如只构建clang和lld就可以避免编译 mlir 和 flang。这一点在生产环境尤其重要因为 mlir 的编译时间和磁盘占用都相当可观。2. 从源码构建llvm-project版本选型与一次完整的CMake配置2.1 为什么选15.0.7而不是mainmain分支几乎每天都有新提交API 也在不断调整。昨天还能用的opt -load写法今天可能就报错明天说不定就换成了另一个 pass manager 调用方式。对想稳定做研究或写工具链的人依赖main是一个灾难。llvmorg-15.0.7是 release/15.x 分支的最终补丁版本。它修复了 15.0.0 到 15.0.6 阶段的不少回归问题CMake 选项相对稳定。尤其是 llvmpipe 在 15.0.x 上的行为很典型很多第三方二进制比如 Mesa 的软件渲染驱动也基于这个版本线。如果照着我下面的方式卡版本能少踩一半的坑。2.2 硬件、磁盘和依赖先给一个最低参考磁盘源码加构建目录至少 20GB想顺手构建 clang 的话建议留 40GB 以上。内存16GB 够用但链接时仍可能内存紧张8GB 机器建议使用lld做链接器并限制并行线程。CPU核心越多越快但内存瓶颈往往出现在链接阶段。依赖方面Ubuntu/Debian 可以装sudo apt update sudo apt install -y build-essential cmake ninja-build python3 zlib1g-dev libxml2-devCentOS/RHEL 则用dnf install gcc-c cmake ninja-build python3 zlib-devel libxml2-devel。确保 CMake 版本在 3.20 以上LLVM 15 对 CMake 版本有要求太旧的 CMake 会直接报CMake 3.20 or higher is required。2.3 一份可以直接抄的CMake配置下面这套配置我测过多次适合构建一个可用的 LLVM Clang LLD 工具链git clone --depth 1 --branch llvmorg-15.0.7 https://github.com/llvm/llvm-project.git cd llvm-project mkdir build cd build cmake -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_ENABLE_ASSERTIONSON \ -DLLVM_CCACHE_BUILDON \ -DCMAKE_INSTALL_PREFIX$HOME/llvm-15 \ ../llvm ninja -j$(nproc)几个关键选项的含义-DLLVM_ENABLE_PROJECTSclang;lld只构建 Clang 和 LLD不需要 mlir/flang节省大量时间。-DLLVM_TARGETS_TO_BUILDX86只生成 X86 后端。如果你还要跑 ARM 交叉编译或 GPU 后端可以加AArch64;AMDGPU但编译时长会增加。-DLLVM_ENABLE_ASSERTIONSON开启断言。调试自定义 pass 和阅读源码时断言能帮你提前暴露问题。发布环境可以关掉以提高性能。-DLLVM_CCACHE_BUILDON如果安装了 ccache重复编译时会快很多。CMAKE_INSTALL_PREFIX不建议装到/usr/local避免污染系统目录也方便后面切换版本。构建命令里的-j$(nproc)会使用所有 CPU 核心但如果你只有 8GB 内存建议改成ninja -j4否则链接阶段很容易触发 OOM。2.4 构建中的常见坑我在不同机器上编译过多次 LLVM遇到过几个频率很高的报错c: internal compiler error: Killed这是内存不足。解决办法是降低ninja -j的并行度或者限制链接并行任务-DLLVM_PARALLEL_LINK_JOBS2。ld: error: unable to find library -lz缺少 zlib 开发包安装zlib1g-dev。Could not find a matching constructor for initializing X大概率是 clang 和 LLVM 版本不匹配。如果你用系统的 clang 编译 LLVM 15最好升级 clang 版本或改用 GCC 11 来编译。FAILED: bin/llvm-tblgen这通常和磁盘空间不足或者多任务竞争有关清理空间后重新构建即可。建议把整个 CMake 配置命令保存成build.sh后续修改选项时方便复用。3. llvmpipe以LLVM为引擎的软件渲染器如何消费IR3.1 llvmpipe在Mesa生态中的位置llvmpipe不是一个独立的二进制而是 Mesa3D 的 Gallium 软件驱动。它的核心思路是把 GLSL/Vulkan 的着色器编译成 LLVM IR再通过 LLVM 的 JIT 引擎在运行时生成机器码。换句话说它把每秒渲染多少三角形的问题部分转移到了运行时编译着色器的速度上。LLVM 15.0.7 版本下的 llvmpipe已经能较稳定地生成 256 位 AVX2 代码这也是网上偶尔看到llvmpipe (llvm 15.0.7, 256 bits)这种字样的原因。3.2 从GLSL到LLVM IR再到机器码一次简化后的流程如下GLSL 或 SPIR-V 先被转换为 Mesa 内部的中间表示比如 NIR。通过nir_to_llvm或类似路径生成 LLVM IR。LLVM 的优化 pass 对 IR 做处理比如 instcombine、loop vectorize、SLP vectorize。后端 SelectionDAG 或 GlobalISel 完成指令选择。由 MCJIT/ORC 的 JIT 组件在运行时生成机器码并缓存。你可以把第 3 到第 5 步理解为每当一个新的着色器程序出现llvmpipe 就现场编译一次小程序。所以 LLVM 版本越新、后端优化越好软件的渲染性能也会跟着受益。3.3 256位向量路径LLVM如何让软件渲染更快现代 x86 CPU 的 AVX/AVX2 寄存器是 256 位可以同时处理 8 个float。llvmpipe 的目标就是把一堆逐像素运算打包成向量指令。在 LLVM IR 层面单独加两个float是%res fadd float %a, %b如果一次加 8 个float就是%res fadd 8 x float %a, %bX86 后端会把第一条编译成vaddss标量 SSE/AVX 加把第二条编译成vaddps向量加。当启用 AVX2 并允许 YMM 寄存器时vaddps可以一次处理 8 个单精度浮点。llvmpipe 在生成 IR 时往往会构造较宽的向量然后让 LLVM 的向量化 pass 进一步合并相邻运算。最终渲染提速的本质是减少了指令数而不是提高主频。3.4 什么时候你真的会用到llvmpipe服务器或云主机没有 GPU需要跑 OpenGL 应用。CI 环境里做渲染回归测试不能用硬件 GPU 保证一致性。调试 Mesa 驱动本身的渲染逻辑。在某些虚拟化环境里GPU 直通不可用llvmpipe 是最省事的软渲染方案。实际使用中 llvmpipe 画桌面、看视频是可以的但大型 3D 应用会明显吃力。理解它对 LLVM 的依赖就能明白为什么 LLVM 后端向量化质量会直接影响这种软件渲染器的上限。4. 用一个向量加法例子看懂LLVM的优化流水线4.1 从Clang到LLVM IR先写一个足够简单的 C 函数// vector.c void vector_add(float *a, float *b, float *c, int n) { for (int i 0; i n; i) { c[i] a[i] b[i]; } }用刚才构建的 clang 生成 IR$HOME/llvm-15/bin/clang -O2 -S -emit-llvm vector.c -o vector.ll打开vector.ll在 LLVM 15 的默认-O2下循环是否被向量化取决于目标 CPU 特性。如果你没有加任何-march可能只会看到标量循环也可能看到fadd float而不是8 x float。这是因为较老的默认目标比如x86-64没有启用 AVX2。如果想看到宽向量直接加$HOME/llvm-15/bin/clang -O2 -mavx2 -S -emit-llvm vector.c -o vector.ll这时候 IR 里很可能出现8 x float的运算。LLVM 10 以上的自动向量化已经很强但它通常要求循环内没有复杂的指针别名问题。上面这个循环的数组指针如果可能互相重叠编译器会产生一个标量版本用于处理边界。4.2 opt优化管线和pass的作用LLVM 自带一个叫opt的工具可以单独运行某个 pass 或一串 pass$HOME/llvm-15/bin/opt -passesdefaultO2 vector.ll -S -o vector.opt.lldefaultO2会展开成一长串 pass包括instcombine、loop-vectorize、slp-vectorize、gvn、licm等等。如果想看每一步到底改了什么可以$HOME/llvm-15/bin/opt -passesdefaultO2 -print-after-all vector.ll -o /dev/null 2 after.log这个日志会非常大所以更好用的是加一个函数过滤器$HOME/llvm-15/bin/opt -passesdefaultO2 -print-after-all \ -filter-print-funcsvector_add vector.ll -o /dev/null 2 after.log然后直接grep add 8 x float after.log能看到向量化 pass 在哪一步插入了宽向量计算。4.3 llvmpipe是如何选择向量宽度的llvmpipe 的代码在构建LLVMContext和TargetMachine时会根据 CPU 特性开关决定是否启用 AVX2。如果启用了 AVX2一些内部 pixel block 的尺寸就会按 8 个单精度浮点来设计。你也可以用llc直接查看 X86 后端的最终汇编$HOME/llvm-15/bin/llc -marchx86-64 -mattravx2 vector.ll -o -如果向量化成功你会看到类似vaddps %ymm0, %ymm1, %ymm2的指令其中的vaddps就是 256 位向量加法。这个细节解释了网络热词里的256 bits它不是指内存地址宽度而是编译器在后端为软件渲染选定的向量位宽。5. 写一个自定义LLVM Pass把为什么一次讲透5.1 为什么要自己写Pass自定义 pass 通常用于三类场景做源码级性能分析、在 IR 层面插桩、实现团队特有的优化。虽然 LLVM 自带几百个 pass但真实项目里总有如果编译器能在这里吐一行日志的需求。LLVM 15 默认使用 New Pass Manager所以我们要基于PassInfoMixin来写。老式的FunctionPass在 15 里仍然能编译但官方已经不推荐我建议直接从新的写法开始。5.2 代码骨架创建一个MyPass.cpp#include llvm/IR/Function.h #include llvm/IR/LegacyPassManager.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h #include llvm/Support/raw_ostream.h using namespace llvm; namespace { struct MyPass : public PassInfoMixinMyPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { errs() Function: F.getName() \n; return PreservedAnalyses::all(); } }; } // namespace 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(); }这段代码的核心思路是MyPass继承了PassInfoMixinMyPass这是 New PM 对函数 pass 的基本要求。run方法接收当前函数和函数分析管理器返回PreservedAnalyses。没有修改 IR所以返回PreservedAnalyses::all()告诉框架分析结果可以全部保留。llvmGetPassPluginInfo是插件入口opt 加载.so时会查找这个符号。5.3 编译和运行编译插件时强烈建议使用你刚构建出来的llvm-config而不是系统自带的$HOME/llvm-15/bin/llvm-config --cxxflags $HOME/llvm-15/bin/llvm-config --ldflags --libs然后编译clang -fPIC -shared MyPass.cpp -o libMyPass.so \ $($HOME/llvm-15/bin/llvm-config --cxxflags --ldflags --libs)运行$HOME/llvm-15/bin/opt -load-pass-plugin./libMyPass.so \ -passesmy-pass vector.ll -S -o /dev/null如果一切正常你会在终端看到每个函数的名字。这证明插件已经成功加载。5.4 一个真实的坑Pass注册名与插件加载我在给朋友调这个流程时遇到最多的错误是Attempting to load a pass plugin created with incompatible LLVM version.原因几乎都是用了系统里另一个 LLVM 版本的llvm-config来编译插件然后把插件加载到 LLVM 15 的opt中。New PM 的插件注册表包含版本信息任何不一致都会直接拒绝加载。解决办法就是让编译插件和运行opt使用同一个llvm-config。如果你构建的是llvmorg-15.0.7那就用build/bin/llvm-config不要再用/usr/bin/llvm-config。另一个易错点是如果你的 pass 修改了函数体但没有把相应分析标记为PreservedAnalyses::none()框架可能还使用旧的 dominator tree 或 loop info导致后续 pass 崩溃。所以没改动就返回all()改动了就要谨慎处理。6. 给想啃llvm-project源码的人一些建议6.1 从哪几个目录开始读很多人打开llvm-project就蒙了目录太多不知道从哪下手。我的建议顺序是llvm/include/llvm/IR和llvm/lib/IR先把 Instruction、BasicBlock、Function、Module 这些核心数据结构看懂。llvm/lib/Transforms/Utils里面有一些通用工具比如本地代码提升、循环简化很多 pass 都依赖它们。llvm/lib/CodeGen/SelectionDAG如果你想深入后端这是指令选择的关键。llvm/lib/Target/X86结合具体后端代码理解指令选择和寄存器分配。TableGen.td文件可以先不深究我是在需要改 target 时才回过来学的。6.2 一个高效阅读IR方法静态读代码容易陷入细节我比较推荐用工具照镜子的方式。比如想看某个函数经过多次优化后变成什么样可以opt -passesdefaultO3 -print-after-all \ -filter-print-funcsvector_add vector.ll -o /dev/null 2 after.log然后打开after.log一步一步跟着 IR 变化走。这比在源码里猜某个 pass 的意图要直观很多。我写自定义 pass 时也经常先用这个方式观察目标函数看看插桩应该放在哪一阶段。6.3 哪些部分可以暂时跳过llvm/lib/Transforms/Vectorize里的 SLP 和 Loop Vectorize 启发式非常复杂初学没必要逐行读。各个后端的*ISelLowering.cpp动辄上万行且很多规则由 TableGen 生成硬读效率低。mlir整个子项目自成体系如果目标不是 MLIR可以先完全不看。polly也是高级优化后期有需要再学。真正需要反复读的其实是llvm/lib/IR和llvm/lib/Transforms里的核心 pass这些才是理解整个优化流水线的钥匙。说实话llvm-project是那种一次看不完、越看越觉得大的项目。最初我把它当成一个大而全的编译器后来才发现真正的价值是那些可以拼装的库。llvmpipe 的 256 位路径让我第一次直观感受到编译器后端的选择会直接跑到渲染性能上而写自定义 pass 则让我意识到优化不是玄学。如果你也在折腾这个项目建议先固定一个发布版本比如 15.0.7把构建脚本保存下来再慢慢深入。遇到链接问题优先考虑版本一致性遇到向量化问题就打开-mattr盯着llc输出。跑通一遍之后你会发现 LLVM 的设计其实有一条非常清晰的主线。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价