
如果你在日志里看到过一行llvmpipe (LLVM 15.0.7, 256 bits)大概率第一反应是“显卡驱动是不是出问题了”。实际上这串字符背后站着的是整个开源编译界绕不开的一个巨物llvm-project。它是 LLVM 编译器基础设施的主仓库也是目前世界上最复杂的 C 项目之一。无论是你写 Rust、Swift、Kotlin 时用的编译器还是 Chromium、Android、iOS 里的工具链抑或是跑在云服务器上没有任何 GPU 也能渲染图形的那层软件层都直接或间接建立在llvm-project之上。这篇文章不打算做成“LLVM 源码导读”太劝退。我想从一个实际开发者的视角带你把这棵仓库的目录树拆开来看讲清楚它为什么这么设计、怎么上手构建、那个256 bits又是怎么来的以及初学者最容易踩的坑是什么。无论你只是想搞懂热词还是准备真正把 LLVM 用起来这篇都能当做一个入口。1. 一个目录树装下整个编译器生态llvm-project 仓库到底长什么样1.1 从 SVN 到 monorepo为什么官方要合并成一个仓库很多人第一次打开llvm/llvm-project的 GitHub 页面都会被目录数量吓到。官方仓库采用单一代码仓库monorepo模式所有子项目都放在同一个版本号下用同一个构建系统管理。这个局面不是一开始就有的——早年 LLVM、Clang、LLD 等各自有独立的 SVN 仓库版本号还经常对不上。2019 年官方做了重大迁移把所有相关项目收拢到 Git 的 monorepo 中。核心原因很实际LLVM 子项目之间存在非常密切的交叉依赖比如clang依赖于LLVM核心库lldb又依赖clang的很多前端能力。如果它们各自独立发版那么“某个版本的 clang 配合某个版本的 lld 才能正常工作”会变成一场灾难。合并成一个仓库后一次提交可以同时修改核心库、前端、链接器并且所有这些变更处于同一个 commit 里原子性大大增强。你在做跨项目改动时再也不用担心改完LLVM忘了同步提交clang的适配代码。现在你在 GitHub 看到的就是一个整体快照llvm目录是核心其他目录都是围绕它生长的生态。理解这个仓库其实只需要抓住一条主线核心库负责做优化和代码生成围绕它的是各种语言前端、工具链组件和运行时库。1.2 核心子项目导览LLVM、Clang、LLD、libc、MLIR 各自管什么我先用一张表把这几个高频目录的功能说清楚后面再深入讲它们的协作方式。目录作用一句话理解llvm/编译器基础设施IR 定义、优化 Pass、后端代码生成、JIT 引擎、汇编器等整个项目的中枢神经系统clang/C/C/Objective-C 前端能把hello.c变成 LLVM IR 的“翻译官”lld/链接器目标是替代系统自带 ld快内存占用低跨平台libcxx/libcxxabi/libunwind/C 标准库、ABI 兼容层、栈展开器新架构/新系统上跑 C 的基石compiler-rt/运行时支持库sanitizer、profile、内置函数让编译产物在目标机器上真正跑得稳mlir/多层级中间表示框架面向编译器和机器学习编译器深度学习的编译器基础设施也靠它flang/Fortran 前端老牌科学计算语言的新实现lldb/调试器看代码行号时它不只是跟 gdb 类似的工具openmp/OpenMP 并行运行时多线程并行编程的实现机制polly/基于多面体模型的循环优化让循环变换和自动向量化做得更聪明libc/正在增量实现的 C 标准库想把系统级依赖全部自有化bolt/二进制优化工具对编译产物做性能剖析后重新布局clang是绝大多数人接触 LLVM 的第一站。它的作用就是把 C/C 代码转换成 LLVM 的中间表示IR。举个例子你写一个简单的加法函数int add(int a, int b) { return a b; }用clang -S -emit-llvm add.c -o add.ll生成出来的就是一个.ll文件里面是 LLVM IR 的文本形式。这个 IR 大致的模样是define i32 add(i32 %a, i32 %b) { %1 add nsw i32 %a, %b ret i32 %1 }你可能已经看出来了i32表示 32 位整数add nsw是一个带“无符号溢出未定义行为”标记的加法指令。整个 IR 的定位是“介于高级语言和汇编之间”它保留了很多优化所需的信息又不会绑定到某个 CPU 架构上。有意思的是clang还有“驱动模式”的概念。你直接在命令行敲clang hello.c -o hello它不只是编译器还会自动调动汇编器llvm-mc和链接器lld整个工具链被一个命令串起来了。这也是为什么 LLVM 生态能替代传统 GNU 工具链的原因之一。2. 追溯“为什么需要 LLVM”编译基础设施的设计哲学2.1 传统编译器前端与后端耦合的困局想要理解llvm-project为什么长成这样必须先回到一个传统问题写编译器为什么这么贵在 GCC 统治的时代编译器通常是一个“整体”每种语言对应一个前端每个 CPU 架构对应一个后端前端和中间优化与后端深度耦合。你想为新的语言写编译器就要连优化器和所有目标平台的后端一起写你想支持新的 CPU 架构就要把所有语言前端都适配一遍。这导致两个改动方向上的成本都极高。LLVM 的反传统设计在于它把编译器拆成三个可以独立演进的阶段前端Frontend - 中间表示IR - 优化器与后端Backend对应到llvm-project里clang是前端llvm/lib/Transforms是优化器llvm/lib/Target/*是后端。前端只需要生成符合 LLVM IR 规范的文件优化器只负责对 IR 做统一处理后端只负责把 IR 翻译成目标机器的汇编或机器码。三层各管各的接口就是 IR 本身。这个设计最直接的好处是你每多支持一种编程语言只需要写一个能生成 IR 的前端每多支持一种 CPU 架构只需要写一个能从 IR 生成目标代码的后端。Rust 的rustc起初用的就是 LLVM 后端Swift 从第一天起就构建在 LLVM 上Julia 也通过 LLVM JIT 把动态函数编译成高性能机器码。大家都是看中了这一套“基础设施复用”。2.2 SSA 与 PassLLVM IR 为什么适合做全局优化LLVM IR 之所以被这么多项目接受核心在于它使用静态单赋值Static Single AssignmentSSA形式。用我的话解释SSA 就是“每个变量只被赋值一次”如果后续代码需要给它赋新值那就重新造一个新变量名。举个例子普通 C 代码里你可能会写int x a; if (cond) { x b; } return x;转化为 SSA 形式后x会出现多个版本最后通过phi指令合并entry: br i1 %cond, label %then, label %else then: %x.1 load i32, i32* %b_ptr br label %merge else: %x.2 load i32, i32* %a_ptr br label %merge merge: %x.phi phi i32 [ %x.1, %then ], [ %x.2, %else ] ret i32 %x.phi这种看似别扭的表示却让优化器可以做非常多舒爽的事情因为每个值的使用链是明确的数据流分析变得极其简单因为变量不可重复赋值许多优化算法可以少做很多“别名猜测”。优化器在 LLVM 里以 Pass 的形式存在。一个 Pass 就是一个编译过程中的运行单元只做一件具体的事。比如-instcombine专门做常量折叠和指令合并-loop-unroll专门做循环展开-gvn做全局值编号。你用opt -passes...就可以像搭乐高一样组合它们。这个 Pass 架构的收益是每次 LLVM 版本升级都会引入新的 Pass但旧的功能依然能通过组合保留下来同时Pass 之间通过 IR 衔接可以穿插在任意两个阶段之间非常灵活。2.3 它不只是编译器LLVM 作为 JIT 引擎的一面如果没有进一步了解很多人会以为llvm-project就是“用来写编译器的库”跟普通业务开发没关系。但要理解热词llvmpipe就必须认识到LLVM 核心是带 JITJust-In-Time能力的运行时引擎。LLVM 的ExecutionEngine和现在的ORC JIT接口可以做到在程序运行时把 LLVM IR 动态编译成当前 CPU 能执行的机器码。用它在运行时“把中间表示翻译为可执行代码”跟“把 C 代码提前编译成可执行文件”是同一套后端代码路径只是触发时机不同。这个能力让 LLVM 在很多日常软件里扮演了隐藏角色。比如PostgreSQL在表达式求值时会用 LLVM JIT 编译过滤条件减少解释执行开销。Numba把 Python 函数的字节码翻译成 LLVM IR再 JIT 成机器码让 Python 写数值计算接近 C 速度。Julia的默认编译器就是 LLVM每个函数第一次被调用时都会被 JIT 编译。显卡驱动领域的llvmpipe也把着色器编译成 CPU 能并行执行的 SIMD 机器码。理解了这条线你就会发现 LLVM 不是某个角落里的冷门项目而是现代编程语言高性能实现、数据库、图形渲染等多个方向的公共底座。3. 动手构建 llvm-project从源码编译到第一次运行3.1 环境准备与硬件规划在真正开始折腾之前我想先说句公道话构建 LLVM 是目前开源项目里少有的“环境要求高、容易劝退新手”的操作。但只要你按合理的步骤走它又能给你带来极强的“工具链掌控感”。硬件方面我的建议是内存至少 16GB低于 8GB 会非常痛苦。磁盘至少准备 80GB 以上的空闲空间。Release 构建加工具链全部产物通常会在 50GB 左右如果开启 Debug 符号空间需求会翻倍。多核 CPU 对编译速度影响最直接。LLVM 是一个极大地被并行编译受益的项目核心数越多越好。系统方面Linux 和 macOS 体验最顺Windows 上也能构建但建议用 Visual Studio 的x64 Native Tools Command Prompt来执行 CMake用clang-cl或 MSVC 作为构建编译器。3.2 CMake 配置里最容易踩的坑LLVM 的构建使用 CMake官方推荐的配置方式大概是这样git clone --depth 1 https://github.com/llvm/llvm-project.git cd llvm-project mkdir build cd build cmake -G Ninja ../llvm \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_USE_LINKERlld \ -DLLVM_CCACHE_BUILDON如果你照抄这串命令它已经规避掉了大部分新手坑。逐个解释一下-DLLVM_ENABLE_PROJECTS控制你要构建哪些子项目。默认情况下只构建 LLVM 核心如果你想用clang编译 C 代码必须把clang放进这个列表。这里用分号分隔字符串里需要加引号。-DLLVM_TARGETS_TO_BUILD是最容易被忽略、却对构建时间影响最大的选项。LLVM 核心默认会构建所有支持的 CPU 后端包括 X86、ARM、AArch64、RISC-V、PowerPC、Mips、WebAssembly 等十几套。每套后端都是一大堆 TableGen 生成的代码和匹配逻辑。如果你只是在 x86 机器上学习只保留X86能把编译时间缩短到原来的三分之一甚至更少。-DLLVM_USE_LINKERlld是用 lld 做链接器。这个选项有点鸡生蛋蛋生鸡的味道你要构建 lld但又想用 lld 链接 lld。CMake 的解法是如果系统里已经有 lld 可用而它又支持-fuse-ldlld构建系统就会调用它。没有 lld 的话也可以用系统自带的gold或ld只是链接时间会慢不少。-DLLVM_CCACHE_BUILDON是开启 ccache 编译缓存。LLVM 每次重新跑 CMake 后改动一个头文件就可能触发大量 C 重编译。ccache 能把基于相同编译器、相同头文件状态的编译结果缓存起来显著提升迭代重建速度。实际使用中它对我这种反复改配置的人帮助特别大。构建指令也很简单cmake --build .但我不建议直接用cmake --build .不做任何限制尤其是内存不足的机器上。更稳妥的是指定并行度比如ninja -j 8我见过不少人直接跑到ninja -j让 Ninja 自动全核编译导致内存耗尽最后连图形界面都卡死。LLVM 的编译进程是出了名的内存大户每个编译单元可能就要占用 2GB 左右内存。8 核 16GB 的机器上-j 6比较安全。3.3 验证工具链让你的 clang 编译第一个程序构建完成之后.build/bin目录下会出现clang、llc、opt、lld、llvm-readobj等一系列工具。这时候可以做一个简单的自验证。先准备一个 C 文件#include stdio.h int main() { printf(hello, llvm-project\n); return 0; }用你刚刚构建出来的 clang 编译./bin/clang -O2 hello.c -o hello ./hello如果能正常输出说明整个编译管线已经通了。接下来我建议你多做一个动作把编译过程拆开来看。./bin/clang -S -emit-llvm hello.c -o hello.ll这是 clang 完成的“前端工作”把 C 代码翻译成 LLVM IR。生成出的hello.ll里会有main、puts等符号但还没有任何机器码。再执行./bin/llc hello.ll -o hello.s这次是把 IR 变成 X86 汇编。llc是 LLVM 的后端工具。你可以在生成的汇编里看到movl、leaq之类的 x86 指令。看到这一段的瞬间你对整个编译流程的理解会变得完全不同——原来 C 代码要经过这么两层最终才落到机器指令上。4. 从热词llvmpipe (LLVM 15.0.7, 256 bits)看 LLVM 在图形栈中的应用4.1 llvmpipe 是什么为什么它和 LLVM 绑定在一起llvmpipe这个词对多数普通用户来说很陌生但它其实一直潜伏在你身边。它是 Mesa 图形库中的一款软件渲染器实现。所谓软件渲染器就是不依赖独立显卡完全用 CPU 完成图形绘制工作的 OpenGL/Vulkan 实现。Mesa 的 Gallium 架构把驱动分成几个模块llvmpipe就是其中一种“管道”pipe。它的特别之处在于渲染过程中需要把高级着色器语言如 GLSL 或 SPIR-V翻译成 CPU 能执行的机器码。Mesa 自己不想给每个 CPU 架构都写一套汇编级着色器编译器于是干脆调用了 LLVM。llvmpipe (LLVM 15.0.7, 256 bits)这行字符串通常出现在你运行glxinfo或某些图形调试工具时。它表示当前用于软件渲染的 Mesa 组件明确标记了自己编译使用的 LLVM 运行时库版本是 15.0.7同时后端正在用 256 位向量宽度生成机器码。也就是说LLVM 在这条链路里不是一个“静态工具链”而是一个运行时组件Mesa 进程启动后会把着色器逐个交给 LLVM JIT由它根据当前 CPU 的 SIMD 能力生成可执行代码。这也是为什么你会看到“llvm 15.0.7”出现在一个图形软件版本字符串里。它不是在说自己包含了某个编译器而是在告诉你这个渲染器正在调用 LLVM 的运行时能力。4.2 256 bits 的含义SIMD 向量宽度与后端代码生成256 bits对 CPU 指令集有点了解的人可能会立刻想到 AVX2。没错这里标记的就是 llvmpipe 在后端代码生成时选择的向量宽度。现代 CPU 支持单指令多数据SIMD指令可以在一个指令周期内同时处理多个数值。SSE 是 128 位AVX2 是 256 位AVX-512 是 512 位。图形像素操作天然适合这种并行比如对一个 8 像素的 RGBA 区域做颜色变换用 256 位指令一次就能处理 8 个 32 位浮点数比逐像素处理快得多。Mesa 在初始化时检查 CPU 能力如果__builtin_cpu_supports(avx2)返回真llvmpipe 就会让 LLVM 后端生成 256 位宽度的向量代码。所以日志里显示256 bits说明你机器上的 CPU 支持 AVX2并且驱动把这一特性利用上了。如果把场景换到内存带宽有限或 CPU 较老的环境llvmpipe 也可能自动退回到 128 位 SSE 路径。所以这个数字实际上反映了“软件渲染器针对当前宿主 CPU 做的自动优化结果”。它是 LLVM JIT 在现实世界里最典型的动态能力体现。4.3 从着色器到机器码软件渲染里 LLVM 的执行链路很多人不知道运行在 llvmpipe 上的一帧画面背后其实是一条编译器流水线。它大致是这样的应用层通过 OpenGL 或 Vulkan API 提交着色器程序格式可能是 GLSL 源码或 SPIR-V 二进制。Mesa 的着色器编译器shader compiler先做高级优化把 GLSL 翻译成 NIR 中间表示。NIR 再被转换为 llvmpipe 需要消费的表示通常是 TGSI 或直接生成 LLVM IR。关键一步来到了llvm-projectLLVM 优化器对 IR 做向量化、死代码消除、循环优化等处理。随后 LLVM 后端根据当前 CPU 特性从 IR 生成 X86/ARM 机器码。最终渲染循环执行这段 JIT 生成的机器码把像素写入帧缓冲。整个过程中LLVM 既充当了优化器也充当了运行时编译引擎。对一个图形应用用户来说他唯一看到的结果就是即使用户态没有可用的 GPU 驱动画面依然能绘制出来只是性能取决于 CPU 算力而已。这也是我特别想强调的一点很多人在学习llvm-project时只关注“写编译器”却忽略了 LLVM 被大量嵌入式解释器、数据库、脚本引擎、图形运行时作为 JIT 引擎使用。理解 llvmpipe 这条链路能让你对 LLVM 的“运行时价值”有更具体的感知。5. 给初学者的路径建议与常见误解5.1 先跑起来再读代码我推荐的入门顺序面对llvm-project这样一个千万行级的仓库最容易犯的错误就是试图把源码从头读到尾。我自己的真实体会是哪怕只把llvm/lib/IR里的Instruction.cpp完整读透都需要相当长的时间。更合理的方式是带着问题去读从使用工具反过来推代码。建议的路径是这样的先用 3.2 节的命令把 LLVM 和 clang 构建出来确保工具链可用。用opt -passes...跑现成的.ll文件观察不同 Pass 对 IR 的影响。可以先从-mem2reg、-instcombine、-loop-unroll开始。用llvm-mca做机器码分析观察一段代码在特定 CPU 上的流水线执行情况。这个工具非常直观能让你快速理解后端关心什么。阅读官方文档中的MyFirstLanguageFrontendKaleidoscope 教程。这个示例手把手教你实现一门玩具语言并让它生成 LLVM IR、通过 JIT 运行。它是目前公认最好的 LLVM 入门素材。如果对后端感兴趣可以找一个简单架构比如 X86 或 RISC-V的 Target 目录试着添加一条伪指令从 SelectionDAG 的匹配流程走一遍。我把“编译并跑通”放在第一位是因为这个项目如果连编译器都还没构建成功你读文档里的示例代码时很难有实感。一旦你亲手构建出clang再看各种代码生成、优化相关的概念会有一种“原来内部是这么运作的”的踏实感。5.2 几个普遍存在的误区我接触过不少初学者对 LLVM 有一些先入为主的误解。这里挑几个典型的说一下。第一LLVM 不是“完整的 C 编译器”它只是编译器基础设施。真正命令行里被你调用的clang是 LLVM 之上的一个前端项目。你可以把 LLVM 看作“提供后端和优化能力”前端负责把人类语言变成 IR。这也是为什么llvm-project里clang是独立目录。第二LLVM IR 不是字节码虚拟机。虽然它可以有ExecutionEngine动态执行但它不像 JVM 那样定义一套内存模型和类加载机制。LLVM IR 离硬件比 Java 字节码近得多它的目标是在编译流程的不同阶段之间传递代码表示而不是做跨平台应用分发。第三不是所有优化都能“开箱生效”。LLVM 的优化 Pass 需要根据编译选项显式打开。你在clang命令里看到的-O2其实是一组 Pass 的组合。没有开优化时LLVM 生成的 IR 可能非常冗长但这并不说明它不好只是说明“优化还没被请求”。第四版本迭代非常快API 经常变。LLVM 几乎每半年出一个大版本Pass 注册接口、IR 属性、后端接口都可能发生变化。如果你在网上找到一段用法先确认它对应的 LLVM 版本。很多时候你编译报错并不是因为你代码写错而是版本 API 对不上。5.3 这个项目能带给你的长期价值从最功利的视角看熟练使用 LLVM 能带来的职业价值是很明显的编译器开发、静态分析工具、高性能计算、GPU 驱动、语言运行时、数据库优化器等方向都在大规模招聘具备 LLVM 经验的人。但撇开职业价值不谈LLVM 也是我见过的最优秀的大型 C 工程范本之一。你可以从中学习到如何用 TableGen 这种“代码生成器生成器”来维护大规模模式匹配逻辑而不是手写几千条 if-else。如何设计一个可插拔架构让不同团队以模块化的方式贡献代码。如何写单元测试和集成测试LLVM 的lit测试系统有一套非常成熟的 FileCheck 语法。如何通过LLVMContext、Module、Function、BasicBlock等核心数据结构组织一个复杂的 AST/IR 体系。如果你在学习和探索的过程中希望给 LLVM 提交第一行代码我建议你从修文档或者给llvm/lib/Transforms写一个小 Pass 开始。准备一个小实验写一个遍历所有基本块的 Pass找出每个函数里最长的BasicBlock并打印它的指令数量。这个练习比任何教程都更能锻炼你对接口的理解——你既要明白 Pass 的生命周期也要会查阅Function和BasicBlock的 API。我在实际跑通这个实验后才彻底理解了“优化器”这个被反复提到的词究竟在做什么。回到开头那个llvmpipe (LLVM 15.0.7, 256 bits)。当你再看到这串字符时希望你能意识到这行乍看像错误日志的文本其实是 LLVM 这套基础设施在一个非常接地气的领域——软件图形渲染——发挥运行时 JIT 价值的有力证明。从几十年前 Chris Lattner 的一个硕士课题到如今成为几乎所有现代编程语言工具链的“公共底盘”llvm-project的故事本身也是编译技术如何从象牙塔走向每一台设备的故事。如果你想认真研究编译不必从零写一个编译器站在这套基础设施的肩膀上开始就好。