
对“LLVM”这个项目圈内人其实有句玩笑话它是一个“永远在重构自己的编译器框架”。你点开llvm-project这个仓库第一感觉是“怎么这么大”第二感觉是“这哪是个编译器这分明是一整套编译基础设施”。很多人一开始是被 Clang 吸引进来的结果越往下看越发现LLVM 真正可怕的地方不是某个前端有多强而是它把“编译器”这个黑盒子拆成了几块极其清晰的积木然后让所有人都在这些积木上做文章。这篇文章我打算把llvm-project从“项目结构-核心设计-实际构建-坑位排查-生态扩展”这条线完整过一遍重点聊聊 LLVM 15.0.7 时代的一些细节包括很多人会用到的 llvmpipe软件渲染管线常见输出是 256 bits 的 SIMD 向量宽度。无论你是准备入门编译器开发还是想在 Mesa 图形栈里折腾软件渲染或者纯粹想搞清楚clang -O2背后到底发生了什么这篇内容都值得你花十分钟读一读。1. 项目整体拆解LLVM 到底是个什么东西1.1 从“Low Level Virtual Machine”到编译器基础设施LLVM 的缩写原意是 Low Level Virtual Machine但今天你再提“虚拟机”这个概念反而容易误导新人。现在的 LLVM 更像是一套“编译器组件库”它提供的是中间表示IR、优化 pass、目标代码生成、JIT 引擎、链接器、调试器组件等一系列工具链零件。你既可以用它拼出一个完整的 C/C 编译器Clang也可以用它的库去写一门新语言的编译器甚至可以在运行时用 LLVM 做动态代码生成。在我个人看来LLVM 最大的贡献不是“优化有多牛”而是它把编译器的前后端彻底解耦了。老牌编译器 GCC 虽然也分前端和后端但前后端之间的中间表示是松散的、内部耦合的很难被第三方复用LLVM 则把 IR、优化、后端之间的接口变成了稳定的“公共 API”于是任何语言都可以基于 LLVM 写前端任何硬件厂商都可以基于 LLVM 写后端。llvm-project这个 monorepo 里同时包含了 clang、lld、libc、compiler-rt、mlir、flang 等子项目本质上就是告诉你这一整套工具链都是围绕同一个 IR 体系构建的。顺带说一句15.0.7 属于 LLVM 15 系列的一个补丁版本主要修正了 15.0.0 之后的稳定性问题。如果你在生产环境用 15 系列我建议直接上 15.0.7不要停在旧补丁版本。1.2 三阶段架构前端、中端、后端的分工逻辑经典的 LLVM 架构是三个阶段的流水线。前端负责把源代码解析成 AST再降级成 LLVM IR中端是优化器它接收 IR跑几十甚至上百个 pass把 IR 优化成更高效的等价形式后端负责把优化后的 IR 翻译成目标机器的汇编或机器码。这个分工的意义在于写一门新语言的人只需要关注前端写一个新 CPU 后端的人只需要关注后端而中端的优化器是全体共享的。举个例子你写了一个叫 MyLang 的新语言只要它能产出合法的 LLVM IR那么 LLVM 内置的循环优化、内联、向量化、函数合并等几十个优化 pass 全部免费送给你用同时你还自动获得了对 X86、ARM、RISC-V 等架构的支持。这种“一次优化多处受益”的模式是 LLVM 在学术界和工业界同时流行的根本原因。从llvm-project的目录结构也能看出来这个分工llvm/lib/AsmParser负责解析 IR 文本llvm/lib/Transforms放着各种优化 passllvm/lib/Target下面每个子目录就是一个目标架构的后端比如X86、AArch64、RISCV。你打开任何一个后端目录里面都是寄存器描述、指令选择、指令调度、汇编打印器这些模块——这套代码量非常巨大一般初学者不建议直接扑到后端的细节里先从中端和 IR 入手比较友好。2. 核心细节解析IR 的设计魅力与 llvmpipe 的实战价值2.1 LLVM IR为什么静态单赋值是优化器的“氧气”LLVM IR 是一种静态单赋值Static Single Assignment, SSA形式的中间表示。SSA 的核心约束是每个变量只能被赋值一次。听起来很死板但它换来了一个极其强大的性质——数据流关系变得显式化。编译器的优化 pass 想知道“这个值是从哪来的”“这个计算有没有被重复计算”在 SSA 形式下看变量定义边就能直接判断不需要做复杂的数据流分析。这就像你把一团乱麻整理成了编号清晰的流程图后续所有优化都建立在“图”而不是“文本”上面。实际看 IR 的话它的语法有点像 C 和汇编的混血。举一个最简单的例子define i32 add(i32 %a, i32 %b) { %sum add i32 %a, %b ret i32 %sum }i32是 32 位整数类型%a和%b是入参虚拟寄存器%sum是相加结果。这一段 IR 非常直白但它会被优化器处理成各种形态常量传播会把add(2,3)直接折叠成5内联会把函数体嵌入调用点向量化会把多个标量运算合并成一条 SIMD 指令。这就是 llvmpipe 能折腾出“256 bits”这类字样的底层原因——LLVM 的向量化器和后端代码生成会把着色器里的多个标量运算打包成 AVX2/AVX-512 指令一次处理 8 个 32 位浮点。我建议每个想学 LLVM 的人都养成用llvm-dis或clang -S -emit-llvm看 IR 的习惯因为不看 IR你对优化器的理解永远是隔靴搔痒。2.2 llvmpipe当 LLVM 去驱动 GPU 光栅化llvmpipe 是 Mesa 图形栈里的一个软件光栅化器它的全称是“LLVMpipe”核心思路是用 LLVM 的 JIT 引擎在运行时把图形着色器Vertex Shader、Fragment Shader编译成当前 CPU 的机器码然后用 SIMD 指令并行计算像素颜色。传统的软渲染器比如老式的 Mesa softpipe是解释执行着色器每条指令都要翻译分发效率非常低llvmpipe 相当于把着色器变成了“原生函数”再配合 LLVM 的自动向量化让 CPU 模拟 GPU 并行管线的性能大幅提升。我最初接触 llvmpipe 的时候也觉得奇怪现代机器基本都有独立显卡谁还需要软渲染实际场景其实不少服务器环境没有 GPU但需要离屏渲染做测试CI 环境跑 OpenGL 测试用例不能依赖显卡驱动虚拟机和容器里没有 GPU 直通又想验证图形程序逻辑在嵌入式设备或只用 CPU 的机器上跑轻量级图形界面。使用方式也简单设置环境变量LIBGL_ALWAYS_SOFTWARE1再确认 Mesa 编译时打开了swrast驱动就能让 OpenGL 程序走 llvmpipe。如果还开着GALLIUM_DRIVERllvmpipe就明确指定使用 llvmpipe 驱动。渲染结果不会像独显那样流畅但正确性是可靠的对于调试着色器逻辑来说非常方便。2.3 256 bits 的向量宽度CPU 软渲染的加速密码打开 llvmpipe 的时候如果日志或者glxinfo输出里出现了类似LLVM (15.0.7, 256 bits)的字样含义是当前 Mesa 链接的 LLVM 版本是 15.0.7并且 JIT 代码生成最大支持 256 位宽的 SIMD 向量。在这类输出里256 bits 通常对应 AVX2 指令集一次能打包 8 个 32 位浮点数或者 4 个 64 位浮点数。对 llvmpipe 而言SIMD 宽度基本决定了着色器执行的“线程效率”。如果一个像素着色器需要对 4 个像素组成的 2x2 块做颜色计算128 位 SSE 需要分两批执行256 位 AVX2 可以一批完成。所以“256 bits”这两个词看起来简单实际上代表的是底层性能的关键参数。如果你在配置 Mesa 时没有打开对应的指令集支持或者 LLVM 为当前 CPU 检测到的目标特性里没有 AVX2渲染回退到 128 位或者更窄的宽度性能会明显下降。想确认 CPU 支持哪些特性可以看/proc/cpuinfo里的 flags 字段确认存在avx2字样llvmpipe 才会启用 256 位路径。3. 实操过程从零构建llvm-project3.1 环境准备与源码获取我平时用的是 Ubuntu 22.04构建 LLVM 需要的基础依赖包括cmake、ninja-build、gcc或clang、python3、zlib1g-dev。如果还要跑测试记得装litLLVM 集成测试器对应的 Python 包。下面是常用安装命令sudo apt update sudo apt install cmake ninja-build gcc g python3 zlib1g-dev官方推荐的构建工具是 Ninja而不是 make。原因很简单Ninja 的增量构建速度快而且是多核并行调度做得更好。CLion、VS Code 这些 IDE 的 CMake 插件也都天然支持 Ninja 生成器没必要再用 make 折磨自己。接下来克隆仓库。注意llvm-project的历史非常庞大直接 clone 完整历史会很慢我一般用浅克隆加--branch指定版本分支git clone --depth 1 --branch llvmorg-15.0.7 https://github.com/llvm/llvm-project.git这样只拉取 15.0.7 这个 tag 对应的快照省时省流量。如果你只是想看代码而不是构建也可以到 GitHub 的 releases 页面下载源码压缩包。3.2 CMake 配置哪些选项值得认真设置LLVM 的 CMake 选项多到让人眼花缭乱刚上手的人很容易一套默认配置直接开跑结果编译七八个小时或者链接内存爆掉。根据我的经验以下配置影响到体验的核心建议想清楚再设置cmake -G Ninja \ -S llvm-project/llvm \ -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_ENABLE_ASSERTIONSON \ -DLLVM_TARGETS_TO_BUILDX86;AArch64 \ -DLLVM_USE_LINKERlld \ -DLLVM_PARALLEL_LINK_JOBS4这里逐个说明一下我的选型逻辑CMAKE_BUILD_TYPERelease编译优化后的 LLVM 工具链速度很快。Debug 版本适合断点调试 LLVM 本身源码的开发者但效率会差到让人怀疑人生普通使用者不要选 Debug。LLVM_ENABLE_PROJECTSclang;lld默认的llvm核心不包含 Clang 编译器你需要在这里把想构建的子项目加进去。lld是 LLVM 的链接器速度比 GNU ld 快很多构建 LLVM 时用它来链接自身也能省大量等待时间。LLVM_ENABLE_ASSERTIONSONAssertion 能帮你提前暴露很多未定义行为和类型不匹配的问题代价是二进制体积变大、运行时稍慢。我个人建议开不然以后调试插件或者 pass 的时候查错会非常痛苦。LLVM_TARGETS_TO_BUILDX86;AArch64默认会把所有后端目标全编进去包括 AMDGPU、Hexagon、SystemZ 等。如果你只是做桌面开发或学习编 X86 和 AArch64 就够了编译时间能缩短一半以上。LLVM_USE_LINKERlld在已经构建过一次 LLVM 之后系统里可能有 lld 了如果是全新构建建议先确认系统装了 lld没有就sudo apt install lld。用 lld 链接 LLVM 自身比 GNU ld 快得多尤其是最后几个巨大的可执行文件这个差别非常明显。LLVM_PARALLEL_LINK_JOBS4链接阶段内存占用极高一个clang可执行文件的链接可能吃掉好几 GB 内存。如果你机器内存只有 16GB不限制链接并发很可能会直接 OOM。这个参数我基本每次都会设宁可多等几分钟也不能让机器死掉。3.3 编译构建时间和空间的预期管理进入构建目录后执行cmake --build build -j $(nproc)如果用 Ninja更常见的写法是ninja -C build -j 8这里的-j是并发任务数不要一味追求物理核心数。构建 LLVM 时前期的 C 编译阶段确实能吃满核心但到后期的链接阶段内存带宽和内存容量才是瓶颈。我自己的经验是32 核机器上从零构建 Clang lld 大概在 10 到 20 分钟之间内存至少准备 16GB磁盘至少准备 40GB 空闲空间。如果你只想构建llc、opt这些核心工具不想要 Clang那么把LLVM_ENABLE_PROJECTS留空构建时间会大幅缩短这适合只想研究 IR 和优化 pass 的人。构建完成后工具都落在build/bin目录下。我建议先验证一下版本build/bin/llc --version build/bin/clang --version build/bin/llvm-config --version3.4 把 llvmpipe 和 LLVM 版本对齐如果你用的是发行版预编译的 Mesa通常它已经链接了一个系统 LLVM。这时我希望大家注意一个易踩的坑发布版的 Mesa 往往链接的是某个特定 LLVM 版本比如LLVM 15.0.7。如果你自己从源码构建 MesaCMake 会用find_package(LLVM)来寻找 LLVM 库。这里可能会出现“Mesa 需要 LLVM 版本 X但系统装的是版本 Y”的依赖冲突。解决办法有两个用apt install llvm-15-dev这样把指定 LLVM 版本装齐自己构建 LLVM 时设置-DLLVM_INSTALL_UTILSON并把安装前缀交给 Mesa 的 CMake 搜索路径。检查当前 Mesa 使用的 LLVM 和向量宽度可以用glxinfo | grep llvmpipe输出通常长这样OpenGL renderer string: llvmpipe (LLVM 15.0.7, 256 bits)这个信息对排查问题非常关键。如果你改了 llvmpipe 的启动参数、升级了 LLVM渲染器字符串都会变化方便确认是否真的生效。4. 常见问题与排查技巧实录4.1 构建阶段的“拦路虎”我遇到过的第一个高频问题是CMake 版本或编译器版本过低。LLVM 15 对工具链版本有最低要求如果系统自带 CMake 太老中途会出现各种莫名其妙的 configure 错误。建议提前确认版本cmake --version gcc --versionUbuntu 22.04 默认的 CMake 版本3.22 左右是可以构建 LLVM 15 的但如果是更老的发行版可能要升级 CMake。第二个高频问题是磁盘空间不足。LLVM 构建产物非常大build目录动辄 20GB 以上。如果你只分了 30GB 给根分区很可能构建到一半就提示No space left on device。建议提前用df -h看一眼磁盘剩余不够的话把build目录放到其他分区或者清空/tmp下的缓存。第三个问题是链接 OOM。这个在前面提过了LLVM_PARALLEL_LINK_JOBS一定要设置。如果已经 OOM减少这个数字后重新执行 NinjaNinja 会跳过已编译好的对象文件只重新执行失败的链接不用推倒重来。第四个问题Debug 版本慢到离谱。如果你随手选了Debug构建跑opt -O2都比 Release 慢了十倍不止。不要慌这不算 LLVM 的 bug只是断言和未优化代码的代价。如果一开始不想折腾直接用Release构建。4.2 llvmpipe 使用中的典型问题使用 llvmpipe 时用户常问的一个问题是“为什么我的程序没有走 llvmpipe还是用的独显”这多半是环境变量没生效。在 shell 里执行时要这样写export LIBGL_ALWAYS_SOFTWARE1 export GALLIUM_DRIVERllvmpipe glxinfo注意某些应用是 setuid 程序启动时会清理环境变量这种情况需要从父进程层面注入。还有一种可能是系统里同时装了多个 Mesa 版本libGL.so被其他库覆盖导致设置没起作用。这时候可以用ldd查看应用动态链接的 libGL 路径ldd /usr/bin/glxgears | grep libGL确认链接的到底是/usr/lib/x86_64-linux-gnu/libGL.so.1还是 Mesa 构建目录里的库。还有一类问题出在渲染正确但性能很低。这往往是因为 JIT 生成的指令宽度不是 256 位而是降到了 SSE。导致降级的原因有很多最常见的是 CPU 没有 AVX2或者 Mesa 配置时没有对目标 CPU 启用对应指令集。如果你确实在支持 AVX2 的 CPU 上可以试着设置export GALLIUM_DRIVERllvmpipe export LP_NUM_THREADS0其中LP_NUM_THREADS0让 llvmpipe 自动根据 CPU 核心数创建线程池多线程光栅化能压榨出更好的性能。如果你把它设成 1那就强制单线程渲染会明显变慢但便于调试渲染顺序依赖问题。4.3 “这代码是我的 pass 有 bug还是 LLVM 有 bug”很多初学者在写自定义 LLVM pass 时遇到崩溃或者输出不符合预期第一反应是“LLVM 是不是坏了”。以我的经验LLVM 15 之后核心框架已经相当稳定绝大多数问题出在自己对 IR 的不变式理解不到位。比如在 SSA 形式下你不能直接修改一条phi指令的前驱块列表必须通过特定的 API你不能在不知道函数签名的情况下随便替换一个CallInst的返回值类型。遇到这种问题建议先用opt -passesprint...打印原始 IR 和变换后的 IR对比一下差异很多时候问题一眼就能看出来。另外没事开一下-debug参数LLVM 的调试输出非常详细能帮你定位 pass 执行的每个阶段。时刻记住LLVM 的报错信息不是给你看的装饰品它往往直接指出了你违反的约束条件。5. LLVM 15.0.7 的生态扩展与后续学习建议5.1 为什么值得关注 15.0.7 这个版本15.0.x系列在 LLVM 版本演进里是一个“承上启下”的存在。新架构如 LoongArch的后端在这个时期逐步成熟而 MLIR、Orc JIT 这些基础设施也在大踏步往前。15.0.7 作为补丁版重点是修复回归问题和安全漏洞。对普通用户来说如果你从源码构建工具链直接选 15.0.7 就能避开早期版本一堆烦人的 bug。比如 15.0.0 里一些-O2的内联差异在后续补丁中被优化调整15.0.3 之后内存泄漏问题也修了一波。生产环境不要追“最新的小版本竟然比大版本先进多少”这种虚名稳定才是第一位。你完全可以在同一个系统上共存多个 LLVM 版本。Ubuntu 上的 alternates 机制可以让你在llvm-config-15、clang-15之间切换避免破坏系统自带的 GCC 依赖。自己源码构建时只要指定不同的安装前缀如/opt/llvm-15就能和系统 LLVM 完全隔离。5.2 从llvm-project里还能挖出什么宝很多人以为llvm-project就是用来编一编clang其实它内部包含了非常多值得研究的内容MLIR多级中间表示是子项目里的新星它允许你用“方言”定义不同抽象级别的 IR现在的深度学习编译器很多都构建在 MLIR 上TableGen用声明式语言生成指令选择、寄存器描述这类重复性 C 代码读懂 TableGen 之后才算是入门了后端开发的门槛compiler-rt提供了sanitizerAddressSanitizer、UndefinedBehaviorSanitizer等运行时检查库调试内存错误和未定义行为时几乎离不开它libc 和 libcabi是一套新的 C 标准库实现你在用 Clang 编译 C 项目如果不想依赖 GCC 的 libstdc可以切换到 libc。如果让我给一个学习路线建议我会说先玩clang -S -emit-llvm看 IR然后写几个简单的 Function Pass 用opt跑起来再去阅读llvm/lib/Transforms/InstCombine的源码最后挑一个简单后端比如 X86 的子集看 Instruction Selection。这个过程走完你对编译器设计的理解会有一次质的飞跃。关于llvm-project我最后想补充一个心得如果 LLVM 对你有吸引力千万不要被它的代码量吓退。这套代码看起来大但模块划分极其清晰你完全可以在不知道SelectionDAG的情况下先玩转 IR再逐步下沉。每个编译器工程师都是这么一步步“啃”过来的。真正让我对 LLVM 产生好感的时刻不是某个算法优化得多漂亮而是它的生态太会“养人”了——无论是新语言的设计者还是芯片厂商只要接入这个生态就能获得整个社区的积累。这种平台级的效应才是llvm-project最有价值的地方。