我是在一份崩溃日志里第一次完整看到llvmpipe (LLVM 15.0.7, 256 bits)这一串描述的。日志来自一台没有独立显卡的测试服务器程序走了软件渲染Mesa 把 llvmpipe 的驱动信息打到了诊断输出里。当时我正好在折腾从 GitHub 拉回来的 llvm-project 15.0.7一看这行字就明白了软件渲染器并非什么黑魔法它运行时用的就是 LLVM 的 JIT 编译路径。这篇文章我会从 llvm-project 这个仓库聊起讲到 LLVM 15.0.7 的版本定位然后把256 bits背后的 SIMD 向量宽度拆开讲清楚接着说说 llvmpipe 是怎么用 LLVM 在纯 CPU 环境里把着色器“编译”成机器码的最后落到我在真实机器上克隆、配置、编译 llvm-project 15.0.7 的完整过程以及构建阶段反复撞上的几个坑。适合两类人看一种是刚接触 LLVM 但被仓库体积和术语劝退的新手另一种是已经日常使用 clang/gcc、但想知道“LLVM 到底是一个编译器还是很多个编译器”的开发者。1. llvm-project 这个仓库究竟装了多少东西很多人的第一反应是去 GitHub 搜 LLVM然后看到llvm/llvm-project这个仓库十几个 GB 的代码量瞬间就愣住了。它不叫llvm/llvm也不叫llvm/compiler而是叫llvm-project就是因为这个仓库里装的不只是一个编译器核心而是整整一套围绕编译器和工具链的生态。1.1 为什么所有东西都塞进一个仓库这是 LLVM 从 2019 年 10 月开始的 monorepo 结构。在那之前LLVM、Clang、LLD、libc 这些项目各自有独立的代码仓库每次发布版本都要手工保证多个仓库之间的兼容性开发一个新特性往往需要同时跨好几个仓库改代码提交信息也对不齐。后来干脆合并成一个仓库统一管理版本号、统一打 tag这样每次拿到llvmorg-15.0.7这样的标签里面所有子项目都是配套好的不会出现“LLVM 15 配 Clang 9”这种滑稽组合。这个改动对使用者最大的好处是你只需要 clone 一个仓库就能拿到整套工具链的源码。代价也很明显仓库体积巨大全量 clone 非常久所以后面我实操时会用--depth 1做浅克隆。1.2 核心工具链与周边生态一览我把 llvm-project 里常见的目录和用途整理成了一张表方便你对照目录作用日常对应物llvmLLVM 核心包含 IR、优化器、CodeGen、llc/opt/llvm-as 等工具类似 GCC 的中后端clangC/C/Objective-C 前端源码编译成 IR 的入口类似 gcc 本身clang-tools-extraclang-tidy、clangd、clang-format 等辅助工具静态检查/IDE 后端lld链接器速度远快于 GNU ld类似 ld/goldlibc / libcabiC 标准库实现类似 libstdclibunwind栈回溯与异常处理时的展开逻辑系统 libgcc_s 的一部分compiler-rtbuiltins、sanitizer、profile 等运行时库编译辅助运行时lldb调试器类似 gdbmlir面向机器学习领域的可扩展中间表示框架编译器领域的“积木”flangFortran 前端类似 gfortranpolly基于多面体模型的循环优化优化 pass 集合openmpOpenMP 运行时库OpenMP 实现bolt针对二进制的性能优化工具很少直接碰这张表里最容易被新手误解的是llvm这个目录本身。它不是一个完整的编译器而是一套“编译器基础设施”。你可以把它想象成一台发动机Clang 是围绕这台发动机造出来的一辆整车。发动机本身不知道什么叫 C 语言也不知道什么叫int main它只负责把一种叫 LLVM IR 的中间表示优化成适合目标 CPU 的机器码。1.3 子项目之间谁依赖谁搞清楚依赖关系直接决定了你后面 CMake 配置时要勾选哪些组件。clang 依赖核心的 llvm 库因为它要把 AST 转成 LLVM IR再交给优化器和后端。lld 也依赖核心 llvm 库主要是 Support、Object、MC 这些模块。clang-tools-extra 依赖 clang 的库。libc 编译安装时不依赖 clang 的代码但它需要 clang 头文件来获得内建类型信息和一些语法支持。compiler-rt 和 libunwind 比较独立但它们往往需要目标平台的后端信息所以在 LLVM 15 里更推荐放进LLVM_ENABLE_RUNTIMES而不是LLVM_ENABLE_PROJECTS让它们在基础工具链编好之后再编译避免循环依赖。实际动手时如果你只想获得一个能用的 clang lld那就没必要把 mlir、polly、flang 全编译出来它们会白白消耗半小时和一二十 GB 磁盘。先确定需求再决定勾选哪些项目是善用这个 monorepo 的第一步。2. 版本号与 256 bits诊断信息里藏着的 CPU 和向量宽度llvm 15.0.7这个具体版本号不是随便出现的。搞清楚它从哪里来、以及256 bits到底在描述什么能帮你以后看任何编译错误或渲染器诊断信息时少走弯路。2.1 LLVM 的版本节奏与 15.0.7 的定位LLVM 从 4.0 以后放弃了那种 3.9.1 式的版本号改用基于年份的节奏每年一个大版本比如 15.0.0 在 2022 年 9 月发布。大版本发布后后续的 15.0.1、15.0.2 这类补丁版本只修 bug不加新功能也不会大幅改动 API。15.0.7 基本就是 15.x 系列的后期维护版本它在很多 Linux 发行版里被作为系统默认 LLVM 版本使用比如 Debian 12、Ubuntu 23.04 这些。这也是我强烈建议你别一上来就追最新版本的原因。很多下游项目比如 Mesa、Rust 绑定、各种 JIT 应用它们依赖的往往是某个固定范围的 LLVM 主版本。系统里一旦有llvm-config-15这些项目就会找到它。这也是llvmpipe (LLVM 15.0.7, 256 bits)里会出现具体版本号的原因——它并不是说自己“喜欢”15.0.7而是它当时构建时发现系统里只有这个 LLVM 版本可用。2.2 256 bits 到底在说什么先直接说结论在 llvmpipe 的诊断字符串里256 bits指的是 llvmpipe 在编译着色器时使用的最大 SIMD 向量宽度也就是说它能一次让 256 位宽的寄存器同时参与运算。这里涉及 CPU 的 SIMD 能力我建议你用这张表来理解CPU 指令集寄存器宽度一次能处理的 float常见场景SSE / SSE2128 bits4 个所有 x86-64 CPU 基本都有AVX / AVX2256 bits8 个2011 年以后的 Intel/AMD CPUAVX-512512 bits16 个Xeon/部分桌面 CPU在 LLVM 内部优化器会通过 TargetTransformInfo 查询目标 CPU 的寄存器位宽从而决定循环向量化的最大因子。llvmpipe 如果检测到当前 CPU 支持 AVX2那它在把着色器编译成 x86 机器码时就会优先生成处理 8 个 float 的 256 位向量指令。如果 CPU 很老只支持 SSE2那诊断信息里通常就是128 bits。你还可以通过 clang 验证自己机器上的情况# 编译时指定 -mavx2查看生成的汇编里是否用了 ymm 寄存器 echo float dot4(float a, float b, float c, float d) { return a*c b*d; } | clang -O3 -mavx2 -S -o - -x c -生成代码里会看到vmulps %ymm这类 256 位指令。这就是256 bits在 LLVM 世界里的直观证据。需要注意的是诊断字符串里的 256 bits 单纯描述“向量宽度”不是内存地址位数也不是虚拟地址空间大小千万别把 32 位/64 位系统搞混进去。我见过有人看到这段输出以为“256 位 CPU”其实只是 SIMD 向量化粒度。3. llvmpipe 的软件渲染路径没有 GPU 时 LLVM JIT 在干什么llvmpipe 是 Mesa 里的软件渲染驱动。当程序请求 OpenGL 或 Vulkan 上下文时如果系统和环境里没有可用的独立/集成显卡驱动Mesa 就会回退到 llvmpipe。它用 CPU 模拟整个 GPU 渲染管线不需要任何显卡也能画出 3D 画面。但这东西能跑起来靠的不是“一个一个像素慢慢算”这种朴素思路而是 LLVM 的 JIT 编译能力。3.1 为什么软件渲染器需要运行时编译如果 llvmpipe 用最笨的办法拿到一个着色器程序后逐条解释执行那性能会慢到完全不可用。比如一个 fragment shader 要对每个像素做几十次浮点运算1080p 分辨率下每秒要跑几百万个像素解释执行的开销直接乘以几十倍。llvmpipe 的做法是把着色器程序GLSL 编译后的中间表示再翻译成 LLVM IR然后调用 LLVM 的 JIT 引擎在运行时把这段 IR 编译成当前 CPU 的原生机器码。每个着色器只编译一次编译结果被缓存并重复使用之后每帧绘制时CPU 执行的已经是一段针对该着色器专门生成的机器码速度几乎可以和手工写的汇编媲美。这和你写 Python 后用 Cython 把热点函数编译成 C 扩展是同一个思路——解释执行太慢编译成原生码才划算。3.2 从着色器到 LLVM IR 再到机器码的完整路径我来梳理这条路径上层应用调 OpenGL API把 GLSL 着色器源码传给 Mesa。Mesa 先把 GLSL 编译成 TGSI 或 NIR 这类驱动级中间表示。llvmpipe 把这些中间表示转换成 LLVM IR。LLVM 优化 pass 对 IR 做常量折叠、死代码消除、循环向量化等优化。llvmpipe 用 LLVM 的 TargetMachine 类把优化后的 IR 交给 MC 层生成当前 CPU 支持的机器码。生成的原生函数通过 ORC/MCJIT 动态加载到内存之后渲染时直接调用。其中第 5 步到第 6 步是核心也是 llvmpipe 依赖具体 LLVM 版本的原因。它需要链接 LLVM 的X86CodeGen、X86AsmParser、ExecutionEngine这些组件版本不匹配时最常见的结果就是运行时抛LLVM ERROR或者找不到某个目标函数。3.3 256 位向量在这里的实际收益当 llvmpipe 检测到 CPU 支持 AVX2它把 shader 里对颜色、坐标的浮点运算编译成ymm指令一次就能处理 8 个 float。一个四通道颜色值只有 4 个 float所以 128 位 SSE 其实就能装下但 256 位可以让它同时处理两个像素或更大块的局部数据。实际的性能提升不是简单的宽度翻倍因为 llvmpipe 还要考虑分支发散、内存对齐、像素覆盖率等问题不是所有代码都能被向量化。但结论仍然很明确同样一段着色器代码在 AVX2 机器上 llvmpipe 的帧率通常明显高于只支持 SSE2 的老 CPU。256 bits这个数字对调试性能问题是个很有用的信号它告诉你当前 llvmpipe 至少工作在哪个向量化级别上。4. 在真实机器上克隆、配置、编译 llvm-project 15.0.7理论铺垫完了现在进入我自己的实操环节。很多朋友卡在这一步不是因为不会敲命令而是不知道 CMake 里哪些开关重要、哪些坑必须提前避。下面是我完整跑通过的流程。4.1 动手前先根据需求裁剪目标拿到 llvm-project 源码之前先想清楚你要什么。如果只是学习 LLVM IR、跑一下 clang那你的目标就是构建clang和相关工具如果你想研究链接器就把lld加进项目列表如果你想把 libc 作为标准库用那建议通过LLVM_ENABLE_RUNTIMES而不是LLVM_ENABLE_PROJECTS来启用。同时用LLVM_TARGETS_TO_BUILD限定目标后端。大多数人只需要 X86那就老老实实写X86不要用默认的ALL。每多一个后端不仅编译时间大幅增加还可能导致链接内存爆炸。这个选项是我见过新手最常在配置阶段踩的坑。4.2 CMake 配置与构建命令我的推荐命令是# 1. 浅克隆 15.0.7 分支省去一整段 git 历史 git clone --depth 1 --branch llvmorg-15.0.7 https://github.com/llvm/llvm-project.git cd llvm-project # 2. 配置构建目录 cmake -G Ninja -S llvm -B build-release \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld \ -DLLVM_TARGETS_TO_BUILDX86 \ -DLLVM_USE_LINKERlld \ -DLLVM_ENABLE_ASSERTIONSOFF \ -DLLVM_CCACHE_BUILDON # 3. 构建 clang 和 lld 目标 ninja -C build-release clang lld这里每个参数都有明确目的-G NinjaNinja 的并行任务调度比 Make 高效LLVM 这种大规模依赖图的项目在 Ninja 下构建明显更快。-DCMAKE_BUILD_TYPERelease如果要做性能测试必须用 ReleaseDebug 模式会开启大量断言慢到一个数量级。-DLLVM_ENABLE_PROJECTSclang;lld只启用这两个前端不编 mlir、flang、polly。-DLLVM_TARGETS_TO_BUILDX86只编译 X86 后端。-DLLVM_USE_LINKERlld用 lld 做链接器链接 clang 这种巨型 C 程序时比系统默认 ld 快很多内存占用也更稳定。-DLLVM_CCACHE_BUILDON如果你装了 ccache下次重建能省大量时间。初次构建时我建议不要直接ninja -C build-release全量编那样会编出大量你可能永远不需要的测试工具只编clang lld两个目标就行。如果你还需要 llc/opt 这类工具再追加目标即可。4.3 构建后的功能验证构建完成后第一件事是看版本./build-release/bin/clang --version能输出LLVM version 15.0.7就说明基本成功。接下来做一个最小 IR 实验验证 LLVM 的 IR 路径可用echo int add(int a, int b) { return a b; } | \ ./build-release/bin/clang -x c -S -emit-llvm -o - -正常会输出类似define i32 add(i32 noundef %a, i32 noundef %b)的 LLVM IR说明 clang 前端到 IR 的输出通路是通的。想验证代码生成继续用-mavx2编译并看汇编里的向量指令。这一步能直接看到之前讲的256 bits在汇编层面的表现形式。4.4 与系统自带 LLVM 共存自己构建的 clang 放进系统 PATH 后很容易和发行版自带的 LLVM 工具冲突。我的建议是不要覆盖系统路径用绝对路径调用自己的构建产物或者在需要指定编译器时使用export CC/path/to/build-release/bin/clang export CXX/path/to/build-release/bin/clang如果你是给其他项目比如 Mesa 或 Rust提供 LLVM 环境更推荐用pkg-config的--define-prefix或者直接修改LLVM_CONFIG/LLVM_INSTALL_DIR指向自己的构建目录。这样既能用自己的版本又不会影响系统默认工具链。5. 构建和运行阶段我反复撞上的四个坑LLVM 构建本身不复杂但坑都在细节里。这里挑四个我印象比较深的按出现频率排序写给你。5.1 链接器内存溢出最典型的现象构建到一半终端输出Killed或者ld: fatal: Out of memory。这通常发生在链接clang可执行文件本身的时候因为单个 C 目标文件巨大bfd ld 的内存峰值可能冲到 3~5 GB。如果你同时开了ninja -j8那是灾难。我的解决三板斧用 lld 替代默认 ld构建时指定-DLLVM_USE_LINKERlld。加-DLLVM_PARALLEL_LINK_JOBS2限制同时只有两个链接任务避免内存峰值叠加。如果机器内存确实只有 4 GB建议先只构建llvm核心组件或者把-j压低到 2。5.2 系统 GCC 版本过旧导致配置失败LLVM 15 源码要求宿主编译器至少支持 C17官方最低也是 GCC 7.1 起。CentOS 7 上常见的 GCC 4.8 直接会在配置阶段报错报错信息通常带有#error或unsupported compiler字样。遇到这个情况最快的路径不是硬编译源码而是先安装一个新一点的系统编译器或者直接用发行版仓库里自带的 clang 作为“宿主编译器”来构建 LLVM 15.0.7。否则你会卡在配置阶段连 CMake 都过不去。5.3 依赖库缺失导致特性被悄悄关闭LLVM 编译时会自动检测 zlib、zstd、libxml2、ncurses 这些库。如果缺失CMake 不一定会硬报错而是默默把相关功能关掉。等你在 lldb 里使用时发现没有 Python 支持或者某些二进制工具无法读取压缩格式才意识到少了什么。我的做法是构建前先把常见依赖装上sudo apt install zlib1g-dev libzstd-dev libxml2-dev libncurses-dev如果不想额外装依赖也可以在 CMake 阶段显式关闭对应功能比如-DLLVM_ENABLE_ZLIBOFF。关键是你要知道你关了什么不要等运行时报错才回头猜。5.4 Mesa/llvmpipe 与系统里多个 LLVM 版本之间的匹配和本文开头紧密相关的坑在这里。如果你的机器上同时装了 LLVM 14、15、16系统PATH里又有多个llvm-config文件那么构建 Mesa 时很可能会找错 LLVM 版本。llvmpipe 运行时加载 libLLVM.so如果库的版本和编译时不一致轻则性能异常重则直接LLVM ERROR: Tried to execute unknown external function。解决办法是构建 Mesa 时显式指定版本cmake -S mesa -B build-mesa \ -DLLVMON \ -DLLVM_CONFIG$(which llvm-config-15) \ -DLLVM_INSTALL_DIR/path/to/llvm-project/build-release同样运行程序时注意LD_LIBRARY_PATH里尽量不要同时混入多个主版本的 libLLVM.so否则 llvmpipe 在运行时可能加载到错误版本。5.5 多个构建目录配置冲突我在同一棵源码树上建过build-release和build-debug两个目录发现只要目录分开、CMake 的构建定义分开就没问题。但我吃过另一个亏在同一个 build 目录里反复切换-DLLVM_ENABLE_PROJECTS的选项比如先启用了 clang后来又改成 clang;lld中间没有删除整个目录的 CMake 缓存结果编译出一堆奇怪链接错误。正确做法是不同需求用不同目录起名要能一眼区分比如build-release-clang、build-debug-lldb。一旦配置过就别在同一目录里反复改大开关否则就直接rm -rf build-xxx重新来。最后分享一个我自己的固定习惯构建完任何 LLVM 版本我都会立刻留下一个最小验证脚本包含clang --version、一次小 C 程序编译、一次llvm-config --cmakedir查询。因为后面每次重新构建 Mesa、写 Rust 绑定、做 Fuzzing、看崩溃日志都要反复回到这个环境。15.0.7 这套构建目录我在机器上留了相当久直到系统发行版升级才被我清掉。如果你也在同一个版本上折腾 llvmpipe或者只是被 llvm-project 的体积吓到我的看法是先想清楚要哪几块再动手就不亏。
