
我最早被 llvm-project 这个仓库吸引不是因为它是一套编译器而是因为一次排查显卡问题时glxinfo 里蹦出了一行字llvmpipe (LLVM 15.0.7, 256 bits)。一开始我以为系统装错了驱动后来才明白这是 LLVM 的 JIT 编译能力在图形栈里发挥作用的典型场景。从那时起我就把 llvm-project 从“编译器源码”重新定位成“一台可以随时为你生成机器码的引擎”。这篇博文我会把 llvm-project 的仓库结构、三段式编译架构、从源码构建 LLVM 15 工具链的完整过程以及 llvmpipe 这类软件渲染器如何借力 LLVM 的原理讲清楚适合第一次接触 LLVM 的开发者、图形程序调试者也适合任何想在自己项目里集成 LLVM 能力的人。1. llvm-project整体架构与设计思路1.1 上游仓库里到底装了什么llvm-project 是一个多仓库合一的项目把 LLVM 生态里的几乎所有核心组件都收进了同一个代码仓库。这一点很重要因为早期 LLVM、Clang、LLD 等项目各自独立维护版本之间经常出现“互相不匹配”的问题。后来官方干脆把它们合并成 monorepo也就是现在 GitHub 上的llvm/llvm-project你在 Release 页面看到的llvmorg-15.0.7就是整个快照的版本号。仓库里除了核心的llvm目录之外还有clang、clang-tools-extra、lld、libcxx、libcxxabi、libunwind、compiler-rt、polly、flang、mlir、openmp等子项目。以 LLVM 15.0.7 为例llvm核心编译器基础设施包括中间表示IR、优化器、目标后端、llc、opt、llvm-config等工具clangC/C/Objective-C 的前端也是大多数人接触 LLVM 的入口lld官方链接器速度比 GNU ld 快很多内存占用也更低libcxx和libcxxabiLLVM 生态的 C 标准库实现和 ABI 层compiler-rt提供asan、tsan、ubsan等运行时库mlir多层级中间表示框架在 AI 编译器和芯片厂商里用得非常多flangFortran 前端LLVM 15 里已经可以尝试使用。你可以把llvm目录看作“发动机”其他子项目都是围绕这台发动机造出来的整车。这种设计的直接好处是所有组件共享同一套 IR、同一套优化 pass、同一套代码生成后端你不用为每个语言单独写一套优化和汇编生成逻辑。1.2 编译器三段式前端、中端、后端理解 LLVM 架构最关键的是记住“三段式”编译流程前端负责把源代码翻译成中间表示IR中端负责在 IR 上做平台无关优化后端再把优化后的 IR 生成目标平台的机器码。前端对应clang。比如你写一个 C 文件clang会做词法分析、语法分析、语义分析最终生成 LLVM IR。这一步的输出不依赖 CPU 架构无论是 x86、ARM 还是 RISC-VIR 本身是相对统一的。举个例子简单的加法函数int add(int a, int b) { return a b; }执行clang -S -emit-llvm add.cpp -o add.ll你就会看到类似这样的 IRdefine i32 _Z3addii(i32 %a, i32 %b) { %1 add nsw i32 %a, %b ret i32 %1 }IR 一点都不神秘它就是“更接近机器指令的中间语言”变量被拆成一个个%开头的虚拟寄存器控制流用label和br表示。中端的所有优化 pass比如循环展开、常量传播、死代码消除都是在 IR 层完成的。后端对应的工具是llc它把 IR 翻译成目标平台的汇编或机器码。这个阶段会处理指令选择、寄存器分配、指令调度、SIMD 向量化等繁琐问题。正因为三段式拆得足够干净你才能做到“换一个前端就支持一门新语言”“换一个后端就支持一种新 CPU”这也是 Rust、Swift 早期都选择 LLVM 作为后端的原因。1.3 为什么GPU驱动和编程语言都盯上LLVM我见过很多人第一次接触 llvm-project 时有个共同的疑问我只是想写个编译器或者跑个图形程序为什么非要理解 LLVM答案在于LLVM 早已不只是“编译器的工具包”它实际上成了一层很底层的“机器码生成契约”。以软件渲染器 llvmpipe 为例它要做的事情是把 OpenGL / Vulkan 的着色器编译成 CPU 能跑的机器码。如果从头写一个能针对不同 x86 CPU 做指令选择和向量化的编译器后端工作量足以让人崩溃。但借助 LLVMllvmpipe 只需要把着色器翻译成语义清晰的 LLVM IR交给 LLVM 的 JIT 引擎LLVM 后端会自动根据当前 CPU 特性生成 SSE、AVX 或者 AVX-512 指令。这就是为什么 Mesa 项目中会明确依赖 LLVM并且你在渲染器字符串里能看到LLVM 15.0.7这个版本信息。同样的逻辑也发生在 GPU 厂商生态里。NVIDIA 的 CUDA 编译器、AMD 的 ROCm 编译器很多都基于 LLVM苹果的 Xcode 默认使用 clangAndroid NDK 工具链、Chrome 的 V8 也在某些环节使用 LLVM。可以这么说只要你想让代码以“比较高效率”的方式跑在不同处理器上LLVM 几乎是最省事的公共底座。2. 从源码构建一套LLVM 15工具链的完整过程2.1 构建前需要准备的依赖和硬件很多人听到“从源码构建 LLVM”会有点发怵实际上只要环境干净一次成功的概率很高。我建议你在动手前把下面几样东西准备好依赖建议版本用途CMake3.20 及以上生成构建系统Ninja1.10 及以上加速并行构建GCC 或 ClangGCC 7.1 或 Clang 12编译 LLVM 源码本身的编译器Python3.6 及以上部分构建脚本和测试需要zlib1.2.x压缩相关组件需要git近期版本即可拉取源码和切分支硬件方面我个人的体感是磁盘至少准备 20 GB 空闲空间内存最好 16 GB 以上。如果只构建clang和lld需要的空间会少一些但如果你想编全套子项目3040 GB 也很正常。构建内存是最容易卡住的点后面我会专门讲怎么解决链接阶段内存爆炸的问题。值得说明的是如果你只是想用 clang 写普通程序直接用发行版仓库里的二进制包最省事完全没必要从源码编译。自己编译的价值在于一是你需要自定义LLVM_ENABLE_PROJECTS组合二是你想调试 LLVM 源码或开发自定义 pass三是你所在平台没有现成的预编译包。2.2 一份能直接跑起来的CMake配置源码获取用 git 切到指定 tag这里我以 LLVM 15.0.7 为例git clone --depth 1 --branch llvmorg-15.0.7 https://github.com/llvm/llvm-project.git克隆完成后进入仓库。LLVM 的构建约定是cmake时必须指向llvm子目录而不是仓库根目录。我常用的配置命令如下cmake -G Ninja -S llvm-project/llvm -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;libcxx;libcxxabi \ -DLLVM_TARGETS_TO_BUILDX86;AArch64 \ -DLLVM_ENABLE_ASSERTIONSOFF \ -DLLVM_CCACHE_BUILDON \ -DLLVM_USE_LINKERgold这里的参数各有讲究CMAKE_BUILD_TYPERelease表示编译 LLVM 本身时开启优化否则构建出来的clang慢得让人怀疑人生LLVM_ENABLE_PROJECTS决定要构建哪些子项目分号分隔。我只选了clang;lld;libcxx;libcxxabi都是日常最常用的LLVM_TARGETS_TO_BUILD默认会构建所有后端目标包括 X86、ARM、AArch64、RISCV、PowerPC、WebAssembly 等非常耗时。如果你只关心 x86 和 ARM写成X86;AArch64能省下大量编译时间LLVM_ENABLE_ASSERTIONSOFF适合生产使用能提升运行速度开发调试 LLVM 源码时建议设为ON方便暴露问题LLVM_CCACHE_BUILDON会让 CMake 自动配合 ccache这对反复修改源码重新编译的场景帮助巨大LLVM_USE_LINKERgold是给链接阶段减负用的比默认的 GNU ld 快且省内存。如果你系统里已经有lld也可以改成lld内存占用更友好。第一次构建时最好用系统已有的链接器避免“鸡生蛋”的问题。另外如果你只想快速验证可以先编译clang再加上一个lld把libcxx和libcxxabi去掉构建时间会明显缩短。2.3 使用Ninja按需编译和增量加速配置完成后构建就很简单了。Ninja 默认会自动利用多核不过你仍然可以用-j明确控制并行度ninja -C build -j$(nproc)如果你不想一次性全部构建可以指定目标。比如只构建clang和lldninja -C build clang lld这样之后写代码、重新编译 clang 插件都会很快。要是你执行了一次全量构建中途断电或者 CtrlC 中断了也不用担心Ninja 会记录编译状态下次继续时只编译剩余部分。构建过程中你可以做点别的但要注意不要在同一个构建目录里同时跑多份ninja否则会出现.ninja_log冲突。我见过有人为了“加速”在两个终端同时ninja -C build结果不仅没变快反而把内存打满触发 OOM。ccache 在这里的作用很大。第一次全量编译大概需要 3060 分钟取决于 CPU 核数第二次只改动少量源文件时ccache 命中率能达到 90% 以上编译时间可能缩短到几分钟。使用 ccache 前记得确认系统已经安装并且环境变量CCACHE_DIR指向了你想存放缓存的位置。2.4 验证工具链是否可用构建完成后验证三步就够了。先看版本build/bin/clang --version正常情况下你会看到clang version 15.0.7以及 target 信息。然后写个最小程序跑一下build/bin/clang test.cpp -o test ./test最后验证一下 IR 生成能力这能确认 LLVM 后端工作正常echo int add(int a, int b) { return a b; } | build/bin/clang -x c -S -emit-llvm -o - -如果能看到define i32 _Z3addii这类 IR说明从clang到 LLVM IR 这条链路没问题。以后你想开发自定义 pass只要基于这套工具链做增量修改就行。3. llvmpipe软件渲染如何借LLVM起飞3.1 llvmpipe和llvm-project是什么关系先给一个容易混淆的点划清界限llvmpipe并不是llvm-project仓库里的子项目它属于 Mesa 3D 项目是一套纯软件实现的图形驱动。Mesa 在编译时检测系统中的 LLVM通过 LLVM 的 C API 调用 JIT 编译能力所以在运行环境里会看到llvmpipe (LLVM 15.0.7, 256 bits)这样的渲染器字符串意思是“当前软件渲染引擎基于 LLVM 15.0.7使用 256 位向量宽度”。它的典型使用场景包括没有独立显卡的虚拟机、云服务器、CI 容器、远程桌面环境以及某些 WSL 场景下没有 GPU 直通的环境。只要应用程序需要 OpenGL 或 Vulkan而硬件驱动不可用Mesa 就会自动降级到软件渲染llvmpipe就是其中最常用的一套实现。Mesa 里早期还有一个softpipe驱动走解释执行路线性能很差。后来基于 LLVM 重写出来的llvmpipe性能有了数量级提升原因很简单着色器不再是“翻译一层套一层”地慢速解释而是被 JIT 编译成当前 CPU 能直接执行的机器码。3.2 从GLSL到CPU指令JIT编译流程拆解我们以 OpenGL 为例看看 llvmpipe 的工作流程。应用程序调用glShaderSource传入一段 GLSL 着色器源码Mesa 的编译器前端会把它解析成内部中间表示。现代 Mesa 主要使用 NIR最终再转换到 llvmpipe 后端llvmpipe 后端会把这些图形相关的中间表示进一步翻译成 LLVM IR。这一步是整个设计的精妙之处。图形中间表示里虽然也有变量、控制流和函数调用但它仍然带有“着色器语义”比如顶点属性、像素位置、纹理采样等。llvmpipe 需要把这些语义转换成“纯粹计算”的 LLVM IR然后调用 LLVM 的 JIT 引擎在运行时直接生成一段可执行的机器码。JIT 编译完成后同一个着色器的后续执行就不再经过解释器了。比如一个片段着色器要对很多像素做同样的计算llvmpipe 会把像素分批打包交给这段机器码处理。这个思路有点像 Java 的 JIT第一次运行解释较慢后续热点路径直接执行编译后的机器码整体开销被摊薄。对于软件渲染来说这是把 CPU 性能榨干的关键。用一句话概括就是llvmpipe 使用 LLVM 作为“着色器的运行时编译器”让 CPU 扮演 GPU 的角色同时通过 JIT 避免了解释执行的性能灾难。3.3 “256 bits”到底在说什么渲染器字符串里的256 bits指的是 LLVM 后端为目标 CPU 生成的 SIMD 向量宽度。这个信息不是一个固定值它取决于 Mesa 编译时的配置和当前 CPU 支持的指令集。CPU 指令集向量宽度一条指令可处理的 float 数量SSE / SSE2128 bits4 个 floatAVX / AVX2256 bits8 个 floatAVX-512512 bits16 个 float你看到的256 bits说明运行环境支持 AVX2。对于像素类计算256 位寄存器一次可以处理 8 个单精度浮点数相当于同时计算两个 RGBA 像素的全部通道或者对 8 个像素的同一通道做同样运算。软件渲染的瓶颈通常就在这种“对大量同构数据执行相同计算”的场景SIMD 宽度直接决定了理论性能上限。Mesa 构建时会在可用指令集里尽量选最高的那个。如果你的 CPU 只支持 SSE渲染器字符串会显示128 bits如果你手动在容器里禁用了 AVX也会看到类似的降级。这个字符串对排查性能问题很有用明明 CPU 支持 AVX-512但字符串显示 128 bits你就要怀疑 Mesa 编译时没有开启对应指令集或者运行环境的 CPU 特性被容器策略屏蔽了。3.4 用llvmpipe做开发调试的几条实用建议第一如果你想强制所有 OpenGL 程序走软件渲染可以设置环境变量export LIBGL_ALWAYS_SOFTWARE1这能排除硬件 GPU 驱动的干扰对复现渲染 bug 很有帮助。第二想确认当前实际驱动和渲染器信息用glxinfoglxinfo | grep OpenGL renderer第三llvmpipe 是多线程软件渲染如果你觉得性能不够可以用LP_NUM_THREADS控制线程数export LP_NUM_THREADS8默认线程数可能取决于 CPU 核数但有时在容器里检测不准手动设置更可控。第四调试时打开 Mesa 的调试输出export MESA_DEBUG1 export EGL_LOG_LEVELdebug这能让你看到很多 GL 调用层面的错误信息。总体上llvmpipe 适合做图形程序的正确性验证不适合跑重型 3D 应用。它的速度比硬件驱动落后一到两个数量级但作为“保底方案”和“调试基准”价值非常高。4. 构建和渲染环节的常见问题排查4.1 链接期间OOM和内存爆炸的解法从源码构建 LLVM 最常见的翻车现场是编译到后面某个文件时终端直接出现c: fatal error: Killed signal terminated program cc1plus这通常是内存不足导致的。LLVM 的代码体量很大尤其clang前端里动辄几万行的源文件单个编译单元就可能吃好几 GB 内存多个编译任务并行时更容易爆。解决办法可以从几个方向入手降低并行度比如ninja -C build -j2虽然慢但稳定使用更省内存的链接器把LLVM_USE_LINKER设为lld或gold只构建需要的 target不要默认编所有 CPU 后端关掉断言LLVM_ENABLE_ASSERTIONSOFF给系统加 swap至少 8 GB能应急但不治本。如果你内存超过 32 GB 还 OOM就要检查是不是同时跑了太多后台任务或者 ccache 缓存目录所在磁盘满了。还有一种情况是你在容器里构建容器内存限制比宿主机小需要检查 docker/podman 的资源限制。4.2 llvmpipe渲染结果异常的排查思路我在实际使用 llvmpipe 时踩过的坑大部分不是它“跑不起来”而是“渲染结果不对”。比如明明 CPU 支持 AVX2但程序崩溃在奇怪的地址或者渲染出的三角形有裂缝、纹理错位。这时候按顺序排查先确认确实用的是 llvmpipeglxinfo | grep -i renderer如果结果显示llvmpipe (LLVM 15.0.7, 256 bits)再考虑下面几点。第一Mesa 和 LLVM 版本是否匹配。Mesa 是针对特定 LLVM 接口开发的如果你手工编译了较新的 LLVM但 Mesa 还是老版本可能因为 API 不兼容导致 JIT 生成的代码出问题。解决办法是让两者都使用发行版仓库中对应的版本或者同时从源码升级。第二环境变量是否残留了影响渲染的开关比如MESA_GL_VERSION_OVERRIDE设置成过高的版本会让程序走一条 m 并不完善的代码路径。第三用apitrace录制渲染调用回放时对照硬件渲染器和 llvmpipe能快速确认问题出在应用还是驱动。4.3 LLVM 15时代的几个行为变化注意LLVM 每个大版本都会有一些面向使用者的行为变化15.0 也不例外。如果你是第一次用 15.x或者准备从老版本升级下面几个点值得提前了解。第一旧 Pass Manager 的不少遗留接口在 LLVM 15 里已经被移除或不再推荐。写过自定义优化 pass 的人应该有体会迁移到 New Pass Manager 不只是改个注册宏那么简单涉及到 pass 依赖声明方式的改变。第二Clang 15 默认的 C 标准还是 C14从 LLVM 16 开始才默认 C17。如果你从更新版本的 clang 回退到 15代码里用了 C17 特性却忘了加-stdc17编译期报错会让你困惑半天。第三LLVM IR 本身也有演进。某些旧格式的元数据或者属性写法在升级后会被自动升级或直接拒绝。多数情况下llvm-as会给出明确提示但大型代码仓库里藏得深的老 IR 文件也可能成为隐患。一个实用的建议是养成看 release notes 的习惯。LLVM 官方文档里每个版本的 Release Notes 都写得比较详细升级前扫一遍能省下很多排查时间。4.4 常见问题速查表症状可能原因常用解决方式ninja编译时进程被 OOM 杀掉并行度太高或链接器内存占用大降低-j改用lld/gold加 swapc: fatal error: Killed signal单个源文件编译内存爆炸减少并行编译任务确保磁盘有空间cannot find -lz缺少 zlib 开发库apt install zlib1g-dev或对应发行版包clang: error: unable to execute command部分源文件或后端触发 LLVM 内部错误升级 LLVM 补丁版本关闭过度优化渲染器字符串显示 128 bits但 CPU 支持 AVX2Mesa 编译未启用对应指令集或容器屏蔽检查编译参数、CPU 特性尝试更新 Mesallvmpipe 渲染速度很慢线程数不足或 CPU 频率低手动设置LP_NUM_THREADS查看 CPU 频率自编译 LLVM 后 Mesa 渲染异常LLVM 版本和 Mesa 不兼容使用发行版默认的 LLVM 版本或同步升级 Mesa最后分享一点个人经验最后说一点我在实际项目中反复验证过的体会。调试图形代码时我反而会刻意让程序跑在 llvmpipe 上因为硬件 GPU 会掩盖很多状态管理问题而软件渲染会把每一个不合理的 GL 调用都放大错误更容易暴露。对于想深入 LLVM 源码的人来说15.x 是很好的切入版本代码量足够大生态也很成熟既不会像太老版本那样缺功能也不会像太新版本那样频繁遇到 breaking change。构建 llvm-project 这件事只要你完整做成功一次后面再接触 clang 插件、自定义 pass甚至给 Mesa 做软件驱动实验都会顺手很多。如果你在构建或渲染环节卡住了欢迎沿着上面提到的排查思路慢慢试这些东西都是踩过坑之后得出的经验。