近两年我一直想好好梳理一遍 llvm-project 这个仓库,但每次打开 GitHub 都觉得自己被成百上千个子目录淹没。市面上关于 LLVM 的教程大多只讲某一个小点,比如怎么写一个 Pass、怎么用 Clang 的某个 API,很少有人把这个仓库当作一个整体来讲。这篇我打算换个思路,从 "仓库本身" 的视角把这些年接触 LLVM 工具链的经验串一遍:里面到底有什么、它们分工是什么、怎么把它拉下来跑通、哪些雷我已经替大家踩过。内容不会追求编译器理论上的大而全,但尽量覆盖日常开发中真正用得到的东西。
我最早接触 llvm-project 是因为工作里要做一个基于 Clang 的代码规范检查工具。当时一头扎进去,连目录都找不准,后来花了很长时间才把里面几个关键模块的边界捋清楚。这个仓库的定位其实很明确:它不是一个单一软件,而是一整套编译器基础设施的集合。GitHub 上那个叫 llvm-project 的仓库,表面看是一个代码仓库,本质上装着 LLVM 库、Clang 前端、LLD 链接器、libc++ 标准库实现、LLDB 调试器、compiler-rt 运行时库,还有最近几年特别热的 MLIR 子项目。如果你只是做业务开发,当然可以只装官方发布版直接用;但如果你想做编译器相关的研究、性能优化、静态分析,或者想在工具链层面搞点事情,这个仓库就是绕不开的根。
1. 一个仓库装下编译器的一切:先看清 llvm-project 的家底
1.1 主目录结构:LLVM 与 Clang 的边界在哪里
很多人第一次看到 llvm-project 的目录列表都会蒙,因为顶层目录非常多。仅从名字看,有 clang、lld、lldb、libcxx、compiler-rt、polly、mlir、flang、openmp 等,其中 clang 和 lld 这类才是有独立用户界面的产品,而 llvm 这个目录才是整个项目的核心基础设施。
真正要区分清楚的是:LLVM 本身并不包含传统意义上的 "把 C/C++ 翻译成目标机器码" 功能。LLVM 核心库提供的是中间表示(IR)、优化流水线、目标指令描述、代码生成器、JIT 引擎等底层设施。而 Clang 承担的是 "语言前端" 的角色,负责解析 C/C++/Objective-C 源码并生成 LLVM IR。如果用一条流水线来形容编译过程,Clang 负责前半段(源码到 IR),LLVM 负责后半段(IR 到目标汇编、目标机器码),而 LLD 负责最后一步(目标文件到可执行文件)。这个分层设计让 LLVM 成为一个高度可嵌入的基础设施:如果你想做一门新语言,只需要把你的语言编译到 LLVM IR,后面所有优化和代码生成全部复用 LLVM 的现成能力。这也是 Rust、Swift、Julia 这些语言能够快速获得高质量原生编译能力的原因,它们都在不同程度上利用了 LLVM 后端。
在 llvm-project 根目录下,llvm子目录中的include/llvm存放着全部核心头文件,lib/下面是各类库的源码,tools/里则有一些利工具,例如opt(IR 优化器)、llc(LLVM 后端编译器)、llvm-as(汇编器)。Clang 的主要代码在clang/目录中,里面最重要的是lib/AST、lib/Sema、lib/CodeGen这些与语法语义分析和代码生成相关的模块。理解了llvm和clang的分工,后续看任何 LLVM 相关的代码都不会觉得找不到北。
1.2 除了编译器和调试器,还有一堆被低估的宝贝
提到 llvm-project,多数人第一时间想到的是 Clang 优化编译,但仓库里还有很多容易被低估的组件,在日常开发中有极高的实用价值。
LLD:虽然名字叫链接器,但它的性能远超传统 GNU ld。LLD 支持 ELF、Mach-O、COFF 和 wasm 等格式,而且因为它的并行处理能力,链接速度通常比体系链接器快数倍甚至一个数量级。不少大型项目的构建系统已经默认使用 LLD 替代系统 ld,比如 Chrome 与 Android 的构建都受益于它。
compiler-rt:这个运行时库为 Clang 提供各种内建函数和低级运行时支持,例如
__builtin系列对应的一些底层实现、Sanitizer(ASan、UBSan、TSan)等。没有它,编译出的二进制在调试时会缺少很多有用的检查能力。MLIR:这是一个 "编译器中的编译器" 基础设施。它将 IR 拆成多层抽象,允许开发者针对特定领域定义中间表示,然后逐层 lower 到机器码。近年来 AI 编译器领域大量的工作都建立在 MLIR 之上,以至于 llvm-project 的更新日志里,MLIR 相关 commit 数量已经非常可观。
Polly:利用多面体模型做循环变换和自动并行化。虽然在通用编译中不像 O2 那样默认打开,但在高性能计算领域有重要的研究和使用价值。
libc++ / libc++abi / libunwind:这是 LLVM 官方的 C++ 标准库实现。跨平台开发时,用它们可以避免依赖特定平台的 libstdc++ 行为差异,也更容易支持 C++20/C++23 的新特性。
还有一种常见场景是写 Clang 插件或静态检查工具,比如基于 libTooling 做源码级扫描。很多人知道 clang-tidy,却没意识到 clang-tidy 本身也只是 libTooling 上的一个应用。你在 llvm-project 的clang-tools-extra/目录下能找到 clang-tidy、clangd、include-what-you-use 等工具的源码,它们就是学习 libTooling 编程的最佳参考。
2. 从 git clone 到跑通测试套件:构建前的准备和真实耗时
2.1 拉取仓库时的两个必须注意的配置
官方仓库地址是https://github.com/llvm/llvm-project.git,直接git clone就能拿到全部代码。但这里有几个细节,很多人第一次操作时没留意。
第一个是仓库体积。llvm-project 完整克隆下来,git 历史加工作区可能会超过 2GB,普通的浅克隆默认也会拉取全部历史。我的建议是使用--depth=1只拉取最新提交,毕竟绝大多数情况你不是在做代码考古。等到需要看历史版本时再单独 fetch 也不迟。命令大概是这样:
git clone --depth=1 https://github.com/llvm/llvm-project.git第二个是版本分支问题。LLVM 官方有明确的发布周期,主干分支(main)每天都在变化,API 变动非常频繁。如果你是在做课程学习或写工具,不建议直接跟主干跑,否则今天能编过的代码下周可能就编译不过了。建议通过 tag 拉取发布版本,例如 LLVM 18.x:
cd llvm-project git checkout llvmorg-18.1.0发布版的 API 已经冻结,网上资料和困惑也相对好查询,遇到问题更容易在现有文档和社区中找到匹配的讨论。
2.2 CMake 配置:按需选择 project 和 runtime,否则会白白等上几小时
llvm-project 采用 CMake 作为构建系统,用 Ninja 加速并行构建是当前实践中的主流选择。第一次构建时最容易犯的错误是企图编译全仓库,这会消耗几十 GB 磁盘和很长的编译时间。应该只构建你实际需要的组件。
简单说,先把常用的配置参数理清:
LLVM_ENABLE_PROJECTS:指定构建哪些子项目,比较常用的是clang;lld;clang-tools-extra等,它适用于需要编译成可执行工具的组件。LLVM_ENABLE_RUNTIMES:指定构建哪些运行时库,比如libcxx;libcxxabi;compiler-rt。从 LLVM 16 开始,官方推荐用这个参数来构建运行时类组件。CMAKE_BUILD_TYPE:日常使用建议Release,带三个调试符号的RelWithDebInfo也可以,但编译时间会变长。LLVM_TARGETS_TO_BUILD:默认构建所有目标架构(X86、ARM、AArch64、RISC-V 等),如果只在本地用,只指定X86能明显减小编译量。
假如你只是想用 Clang 和 lld 开发,一个最小化的配置示例是:
cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld" \ -DLLVM_TARGETS_TO_BUILD=X86然后执行ninja -C build clang lld,注意不要直接ninja全量构建。这样构建出来的产物已经可以正常用于 C/C++ 编译链接。
以我自己的机器为例,16 核 32 线程的配置下,只编译 clang 和 lld,Release 模式大约需要 10 到 20 分钟。如果你把所有组件全部开起来,几个小时都很正常。另外 CMake 配置阶段建议开启LLVM_CCACHE_BUILD=ON并装好 ccache,增量构建速度能快非常多。
2.3 跑通 check-llvm 与 check-clang:验证你构建出的工具链可不可用
构建完成后,最好跑一遍测试套件,验证这次构建的产品是否正常。ninja -C build check-llvm会执行 LLVM 的单元测试和集成测试,check-clang对应 Clang 的前端测试。时间因机器而异,一般在十几分钟到几十分钟不等,但这一步值得做。我第一次构建完 clang 的时候没跑测试,直接拿来编译项目,结果一个 IR 降级的问题排查了很久,最后翻到测试用例才发现是构建时目标架构没选对导致的。跑一遍测试能帮你把这类基础问题全部暴露出来。
3. 核心组件的分工:LLVM 库、Clang 前端、LLD 链接器、libc++
3.1 LLVM Core:以 Pass 为核心的优化流水线
LLVM Core 可以拆成几个层面:IR 定义与模块管理、Pass 基础设施、分析与变换 Pass、目标描述与代码生成。这里的核心概念是 Pass。LLVM 优化器的基本工作方式就是将一个 module 按顺序经过一系列 Pass,每个 Pass 对 IR 做一次分析和变换。例如InstCombine负责指令化简,LoopUnroll负责循环展开,DeadCodeElimination(DCE)负责删除死代码。你写的每个自定义优化,最终就是注册一个新的 Pass 并插入到 Pass 流水线中。
读懂 Pass 的关键是理解 LLVM 的新旧两套 Pass Manager。旧 Pass Manager 基于llvm::FunctionPass和llvm::ModulePass这类接口,概念简单,但并行化和插桩能力有限。新的 Pass Manager 则有更清晰的依赖声明、更细粒度的分析和执行机制。LLVM 14 之后默认启用新 Pass Manager,大部分新代码都应该按新 Pass Manager 的规范来写。
举个例子,如果你想统计一个函数里的基本块数量,简要的骨架大概长这样(传统写法,便于理解):
#include "llvm/IR/Function.h" #include "llvm/IR/Module.h" #include "llvm/Pass.h" using namespace llvm; namespace { struct CountBlocksPass : public FunctionPass { static char ID; CountBlocksPass() : FunctionPass(ID) {} bool runOnFunction(Function &F) override { errs() << "Function " << F.getName() << " has " << F.size() << " basic blocks\n"; return false; // 未修改 IR } }; } // namespace char CountBlocksPass::ID = 0;这个例子虽然不常用,但把 IR 结构、Pass 生命周期、返回值语义都展现清楚了。返回值表示该 Pass 是否修改了 IR,对于分析和统计类 Pass,必返回 false。
3.2 Clang 的生财之道:不只是 C/C++ 编译器前端
Clang 的定位是 "为 C 家族语言设计的前端",但它的作用早已超出了编译器前端本身。因为整个前端以库的方式组织,你不需要启动完整编译流程,就可以复用它的词法分析、语法分析和 AST 构建能力。
举例来说,在使用 libTooling 编写工具时,你可以自己声明一个FrontendAction,让 Clang 提供 AST,再自己遍历 AST 节点做检查:
#include "clang/Frontend/FrontendActions.h" #include "clang/Tooling/CommonOptionsParser.h" #include "clang/Tooling/Tooling.h" #include "llvm/Support/CommandLine.h" using namespace clang::tooling; int main(int argc, const char **argv) { auto ExpectedParser = CommonOptionsParser::create(argc, argv, llvm::cl::GeneralCategory); if (!ExpectedParser) { llvm::errs() << ExpectedParser.takeError(); return 1; } CommonOptionsParser &OptionsParser = ExpectedParser.get(); ClangTool Tool(OptionsParser.getCompilations(), OptionsParser.getSourcePathList()); return Tool.run(newFrontendActionFactory<clang::SyntaxOnlyAction>().get()); }这段代码是很多基于 clang-tidy 定制的检查工具的原型。你可以在此基础上继承ASTConsumer,在HandleTranslationUnit里遍历 AST,然后实现自己的检查逻辑。这个能力非常实用,因为它意味着你完全可以基于这份开源代码做一个属于自己团队的静态检查器。
另外,Clang 还支持在普通编译过程中通过-Xclang -load -Xclang xxx.so -Xclang -add-plugin -Xclang xxx的方式加载插件。插件机制和独立工具的差别在于:插件直接嵌入集成编译流程,运行时开销更小,适合团队内部统一集成到构建系统。
3.3 LLD:为什么现代工具链都在抢着用这个链接器
链接器在传统印象里是个黑盒工具,但 LLD 的架构要清爽得多。它以子命令的方式区分 ELF、COFF、Mach-O 和 wasm 目标,内部大量使用并行算法,因此在处理超大二进制时依然能保持线性甚至更优的扩展性。
有的团队把 GNU ld 换成 LLD 之后,链接时间能够缩短到原来的五分之一到十分之一。对于每天要重新链接几十次的大型 C++ 项目,这是非常可观的效率提升。而在 LLVM 自身的开发中,使用 LLD 链接调试版时明显能减少每轮编译的等待时间。
使用方式也很简单,一条命令即可:
clang -fuse-ld=lld -o app main.o libfoo.a如果你自己构建了 lld,也可以直接用ld.lld命令替代系统 ld:
ld.lld -o app main.o libfoo.aLLD 还有一个很贴心的功能,--reproduce参数可以生成一个问题重现包。遇到神秘的链接错误或崩溃时,把轻度简化的重现包发给别人或保存下来,比自己描述半天高效得多。
3.4 libc++:标准库实现里的另一种选择
Clang 在 Linux 平台上默认使用的 C++ 标准库通常还是系统的 libstdc++,除非你显式指定-stdlib=libc++。libc++ 的代码实现更加现代干净,在一些新标准特性的支持上走得很前。比如在 macOS/iOS 上,Apple 的默认 C++ 标准库就是 libc++,而很多跨平台项目为了保持行为统一,Linux 上也用它。使用时会涉及两个组件:libc++(标准库实现)和 libc++abi(运行时 ABI 支持),还需要提供对应 C++ ABI 的名称修饰规则,这也是为什么用 libc++ 时往往需要专门构建再引入工具链的原因。
构建 LLVM 自带的 libc++ 时,官方建议把运行时组件都放进 runtime build,直接在 llvm-project 上按如上 CMake 配置触发ninja -C build cxx cxxabi,就可以产出包含头文件和对应共享库的完整工具链目录。
4. 写一个真正能跑起来的 LLVM Pass,并挂接到编译流水线
4.1 最小 Pass 的代码骨架和 CMake 配置
很多讲 LLVM Pass 的教程只给到 "写在源码里" 的步骤,但你实际的工作流应该是把 Pass 直接编译成动态库,用opt命令加载。这里我给一个最小可运行的例子,包含代码、CMakeLists 和验证步骤三个部分。
先准备一个目录结构:
MyPass/ include/MyPass/CountBlocks.h lib/CountBlocks.cpp CMakeLists.txtCountBlocks.cpp里实现一个只统计函数基本块数量并打印的 Pass,参考前文骨架即可。关键在CMakeLists.txt:
cmake_minimum_required(VERSION 3.20) project(MyPass) find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") include_directories(${LLVM_INCLUDE_DIRS}) separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND "${LLVM_DEFINITIONS}") add_definitions(${LLVM_DEFINITIONS_LIST}) add_library(CountBlocks MODULE lib/CountBlocks.cpp ) target_link_libraries(CountBlocks PRIVATE LLVM)注意这里用的是add_library(... MODULE ...),编译出来的动态库并不会自动拷贝进 LLVM 目录,而是作为一个独立.so,后续直接用opt -load-pass-plugin加载。
LLVM 和大多数 C++ 库不同,它不提供真正的.so动态链接库供用户二次链接(在多数发行版里),因此用 CMake 的find_package(LLVM)拿到的是头文件和编译参数,实际代码依然需要编译进模块。
4.2 用 llvm-config 拿编译参数,并让 Pass 生效
官方发布版本通常会附带llvm-config工具,从这个工具可以取得编译和链接 Pass 所需的全部标记:
llvm-config --cxxflags llvm-config --ldflags llvm-config --libs core support但如果你是自行构建,建议直接用cmake -S . -B build -DLLVM_DIR=$(llvm-config --cmakedir)来构建 Pass,CMake 会自动处理所有依赖。
构建完成会生成CountBlocks.so。接着准备一份测试代码:
int foo(int n) { int sum = 0; for (int i = 0; i < n; i++) { sum += i; if (i % 2) { sum -= 2; } } return sum; }先用 clang 生成 LLVM IR:
clang -S -emit-llvm test.c -o test.ll然后加载 Pass 运行一次:
opt -load-pass-plugin ./CountBlocks.so -passes="count-blocks" -disable-output test.ll初次运行成功后,你应该能在终端看到类似Function foo has 8 basic blocks的输出。这一步通过,就说明整个编译、加载、执行链已经完全打通。
4.3 调试 Pass 的两个技巧:打印 IR 和查看统计信息
写 Pass 有两个很常见的调试需求:看 IR 和查数据。
如果想在 Pass 里临时打印 IR,可以用llvm::errs() << F << "\n";,它输出到 stderr,不会影响重定向的标准输出。也可以用-print-after-all参数让 opt 在每次 Pass 执行后都打印一遍 IR,这会非常详细,适合检查某一段代码在哪个优化阶段被改写成什么样。
如果你想验证自己编译出的 Pass 是否真的被执行了,可以用统计对象STATISTIC。它在llvm/ADT/Statistic.h中定义,比如声明STATISTIC(BlocksCounted, "Number of functions whose blocks are counted");,然后在 Pass 内部加++BlocksCounted;。运行opt -stats时,就能在最后看到汇总输出。对于性能分析和验证覆盖率很有帮助。
5. 构建资源、调试手段和我踩过的坑
5.1 磁盘与内存规划:不要低估 LLVM 的食量
很多人第一次构建 llvm-project 时,电脑差点死机。这不夸张,LLVM 的 C++ 模板和 STL 使用极度密集,编译时对内存的消耗非常高。根据源码体量和编译器套件的大小,建议:
- 磁盘空间:全量构建建议预留 50GB 以上,最小化构建至少预留 20GB。建议先
ninja -C build -t clean检查当前构建目录大小,避免出现磁盘写满导致中断的惨痛经历。 - 内存:并行构建时,每个编译任务可能消耗 1~2GB 内存。如果你的是 8 核 16 线程 CPU,最好把并行度上限设到 8 或者 16,否则容易内存不足触发 OOM。Ninja 默认会按 CPU 核数压满,保守一点可以
ninja -j8或ninja -j16。 - 使用 ccache 后,首次构建并没有提速,但后续你修改少量源文件后,命中缓存的重编译任务会极快。强烈建议从一开始就开好 ccache。
另外,Windows 上构建 LLVM 会遇到一些跨平台坑,例如需要 Path length 限制关掉、VCToolsVersion 匹配等。如果只是为了学习和开发,我强烈建议在 WSL2 里做 Linux 构建,这样后续编译和调试都会顺畅很多。
5.2 常见报错和解决方法
我把这几年遇到的高频报错整理成一张表,方便按图索骥:
| 报错/现象 | 常见原因 | 解决方案 |
|---|---|---|
CMake 找不到LLVMConfig.cmake | 没有将build/lib/cmake/llvm加入CMAKE_PREFIX_PATH | 自行构建后,在自定义 Pass 的 CMake 里指定-DLLVM_DIR=/path/to/build/lib/cmake/llvm |
undefined reference to llvm::... | llvm-config --libs没有取全或者顺序不对 | 链接时把 LLVM 库放最后,或者改用 CMake 的LLVMtarget |
GLIBCXX_3.4.xx not found | 系统 libstdc++ 版本过低,但 clang 是从新系统上构建的 | 使用较新的系统镜像,或为工具链显式指定-DCMAKE_CXX_COMPILER=g++-12 |
ninja: error: loading 'build.ninja': No such file or directory | 构建目录里没有配置 | 检查 CMake 的 source 目录,llvm-project 的源码目录应该是llvm/,不是仓库根目录 |
| opt 无法加载 pass 插件,报 format 错误 | pass 插件架构与 opt 版本不一致 | 确保 pass 插件是用同一个 LLVM 版本构建的,不要跨 LLVM 大版本用动态库 |
| 编译中 OOM 崩溃 | 并行度太高 | 降低-j,或改用LLVM_PARALLEL_COMPILE_JOBS=4 |
关于 pass 插件版本不一致这点,我特别提一下。LLVM 没有保证不同小版本之间的二进制兼容性,这意味着你用 LLVM 17 构建出来的.so拿到 LLVM 18 的 opt 上,大概率会加载失败。这在社区里非常普遍。建议在开始构建前就锁定一个 tag,并把这个版本信息写进团队 README。
5.3 使用 llvm-reduce 和 bugpoint 做问题最小化
排查编译器问题的时候最常见的痛点是:一个文件几千行,报错却可能只有一句话,看不出为什么会触发 bug。LLVM 社区意识到了这个问题,提供了一整套问题最小化工具。
llvm-reduce可以在保持某个行为(比如 IR 解析失败、Pass 优化导致 Segfault)不变的情况下,尽可能缩减测试文件大小。它的工作原理是拿一个行为和原始 IR 一致的小文件去重放,反复删除没用的指令、基本块或函数,直到不能再删为止。这个工具几乎可以自动完成,极大简化了写 bug 报告和自测的流程。
另一个经典工具是bugpoint,它面向更 "硬核" 的场景,比如自动定位是哪一步 Pass 造成的崩溃或错误代码。它会二分查找错误优化阶段,大幅缩小你排查指令错误的范围。这两个工具在官方文档里介绍得很少,但实际作用很大。
6. 在实际项目中集成 llvm-project 的成熟套路
6.1 作为子模块与作为独立构建的取舍
llvm-project 体量太大,把它直接作为你项目的 git submodule 非常重,每次更新都可能带来大量同步问题。但很多场景又确实需要精确控制 LLVM 版本,比如开发编译器插件、做源码到源码的转换工具等。这时候怎么取舍?
我目前比较推荐的做法是把 LLVM 作为独立依赖,用CMake的find_package来发现预装版本。开发机上安装一份源码编译版 LLVM,然后自己的项目只需要在 CMake 中声明find_package(LLVM REQUIRED CONFIG)。这种方式能让你自己的项目始终保持轻量,又不会因为手动路径配置乱掉。
对于生产环境,不少团队采用预构建镜像的方式:用一个 Docker 镜像把固定版本 LLVM、Clang、LLD 全部编译好,放到镜像仓库,之后所有使用者FROM这个镜像即可。这样既不污染公共构建环境,也能保证工具链版本的完全一致。我在多个项目里已经验证了这种方式比 submodule 更可控。
6.2 用 clangd 替代 IDE 的智能提示
为 llvm-project 代码库本身做开发时,cmake 会生成compile_commands.json编译数据库,clangd 可以根据它提供精确的代码补全、跳转和重构能力。如果你用 VSCode 或 Neovim,将 clangd 指向这个编译数据库,代码阅读体验会有质的提升。同样的思路也适用于你自己的项目:只要构建系统能产出compile_commands.json,clangd 就能成为你团队所有 C++ 开发者的统一索引引擎。
设置方法很简单,在CMakeLists.txt中加一句:
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)构建时生成compile_commands.json,clangd 会自动读取。它不会像 clang 插件那样介入实际编译,而是用一种后台服务的方式给出建议,性能足够流畅。
6.3 版本升级时的主要 breakage 与迁移路径
如果你长期维护一个基于 llvm-project 的项目,最痛苦的莫过于跟随大版本升级。LLVM 每半年发一个大版本,API 迁移几乎没有停止过。我在升级 LLVM 14 到 15、15 到 16、17 到 18 的过程中总结出几个主要 breakage 来源:
- Pass Manager 迁移:旧体系彻底移除,新 Pass API 的
AnalysisManager相关代码需要改。 - 头文件路径调整:比如原来在
llvm/Support/xxx.h的一些内容可能挪到了llvm/ADT或llvm/IR。 - API 签名变化:例如
IRBuilder::CreateLoad在某个版本后必然扩展了对齐参数,CallInst::Create的参数顺序也有变化。 Type::getInt32Ty这类接口逐渐被更细的类型工厂取代。
面对这些变化,最有效的方法不是硬啃 release notes,而是直接打开编译错误提示,到新源码里搜类似//==---的注释和废弃标记。其实大多数 breakage 都是机械劳动,配合-Werror=deprecated能更快定位到具体位置。真正容易踩坑的是某些静默行为变化,比如默认优化参数、默认目标 CPU 等,这些在 release notes 里往往只用一两行提到,却可能让你的性能测试结果发生明显偏转。
7. 我现在是怎么组织 LLVM 相关工作的(个人经验)
最后分享一点我现在的日常工作习惯。虽然这不是什么高深技巧,但对于刚开始接触 llvm-project 的人来说,或许能节省不少自行摸索的时间。
我通常会把 LLVM 相关的开发拆成两层。第一层是安装在系统里的 "预编译工具链",也就是直接从官方发布的二进制包或官方 Docker 镜像获取 Clang/LLD/libc++,用来处理日常编译链接任务。第二层是本地源码构建的开发环境,只构建需要的 target,用来编写和测试自定义 Pass、Clang 插件、静态分析工具。两层并行存在,互不干扰:日常编译用发布版保证稳定,源码构建用于实验性功能验证。
在源码构建时,我的 CMake 命令一般是:
cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra" \ -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;compiler-rt" \ -DLLVM_TARGETS_TO_BUILD="X86" \ -DLLVM_CCACHE_BUILD=ON这样做有两个好处:一是编译时间可控,二是每次改动源码后,编译增量小、反馈快。如果你还要做 MLIR 相关开发,把mlir加进LLVM_ENABLE_PROJECTS即可,但构建资源消耗会明显上升,建议只在需要的时候开启。
另外,写 Pass 或插件时,我强烈建议先从最小的 "打印优化" 开始。也就是不管你想实现什么复杂逻辑,先让 Pass 能稳定跑在简单的 IR 上,确认加载和执行链没问题,再逐步增加分析逻辑。LLVM 本身非常庞大,如果一开始就把模块依赖拉全,很容易在编译阶段就被 CMake 和头文件依赖的问题耗掉大量时间。从最小可见的步骤切入,后续改进才有明确的方向。
llvm-project 不是那种可以 "速成" 的项目,它更像是一座非常专业的厂房。你不需要一次了解所有车间,但应该知道每个车间是干什么的、从哪里进出、跟哪些环节衔接。希望这篇从仓库视角出发的梳理,能让你在翻开那些密密麻麻的源码之前,先在脑子里装好一张地图。