news 2026/9/20 11:54:41

llvm-project核心解析:掌握IR与Pass,构建自定义编译器生态

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
llvm-project核心解析:掌握IR与Pass,构建自定义编译器生态

干编译器这行,llvm-project 大概是绕不开的一个巨型仓库,也是很多开发者repo清单里的“既想碰又不敢碰”的存在。我第一次拉取它的时候,看到根目录里密密麻麻的工程文件夹,脑子里只有一个念头:这哪儿是“一个编译器”,这分明是一套可以自己造编译器的“工业母机”。如果你是想搞懂现代编译器的整体架构,或者想给自己的语言、框架、AI算子直接吃到几十个CPU/GPU后端优化红利,这个项目几乎是必经之路。这篇内容我尽量用实操角度讲清楚它到底是什么、怎么把它跑起来、以及里面哪些东西值得你花时间去研究。

1. 初识llvm-project:先搞清这个工程里到底有什么

1.1 它不是“一个编译器”,而是“一整套编译器生态”

我在早期阶段踩过最大的认知误区,是把llvm-project理解成“LLVM+Clang的代码合集”。实际上这套东西的边界宽得多:除了核心的中间表示(IR)和优化器,它还包含了C/C++前端Clang、链接器LLD、调试器LLDB、标准库实现libc++、运行时库compiler-rt、循环优化工具Polly,甚至还有现在AI编译器圈子里非常吃香的MLIR基础设施。

它的诞生脉络,简单说就是:早年LLVM只是一个研究性质的编译器基础设施,团队把“前端解析、中间优化、后端生成”三个环节彻底解耦,后来苹果看中这个架构,拉上Chris Lattner搞出了基于LLVM的Clang编译器,慢慢替代掉了GCC在自家生态里的位置。现在这个仓库发展成了一个标准的monorepo,也就是把所有相关子项目放在同一个仓库里做版本管理。

对新手来说,这种大仓库有两个直观感受:一是clone体积很大,动辄几个GB,网络不好真的要等到怀疑人生;二是头文件之间的依赖链非常深,如果直接跑“apt install llvm”去用,其实很难感受到这个项目真正的设计精髓,因为预编译包把很多东西都打包封死了。

1.2 仓库里的关键模块,各自负责什么

llvm-project在顶层目录下划分得非常清楚。我最常用的目录大概就是下面这些:

目录名作用我的使用频率
llvm核心基础设施:IR定义、优化Pass、目标后端、MC层等极高,几乎每天都要碰
clangC/C++/Objective-C前端,负责把源码变成AST再变成IR高,日常编译都靠它
lld高性能链接器,支持ELF、Mach-O、COFF等格式中,做工具链会用到
lldb调试器,和LLVM的底层接口结合紧密中,调试复杂crash会用
mlir多级IR基础设施,适合做AI编译器、硬件加速器工具链高,最近研究最重的部分
flangFortran前端,和某些科学计算场景强相关低,用到时再翻
polly基于多面体模型的循环优化低,但价值很高
libcxx/libcxxabi/libunwindC++标准库实现,含ABI、异常栈展开等中,主要看实现细节
compiler-rt编译器运行时库,比如sanitizer系列都在这高,排查内存问题非常依赖
bolt二进制级别的布局优化工具,出性能问题时有用低,这是一把手术刀

这里面最值得注意的是mlir。我最早以为它是个“实验性玩具”,但后来做AI编译相关的项目,才意识到mlir把“高层领域抽象”和“底层硬件优化”结合的思路有多重要。如果没有mlir,你从PyTorch导出模型到各种NPU/DSP上,基本就是给每个硬件手写一遍做苦力活。

1.3 为什么用monorepo做版本管理

使用过传统多仓库方案的人心里清楚:当一个项目拆成llvm、clang、clang-tools-extra、polly等多个独立仓库时,日常提交和版本对齐就是一场灾难。今天clang的某个commit依赖llvm的另一个commit,明天你release时就要逐个仓库去查依赖树。

monorepo把这个痛点直接干掉了。一次commit把llvm、clang、lld的变动同时放进同一个仓库,上游开发者的side-effect保持一致,CI也能在同一个事务里跑完各个模块的测试。对使用者来说,checkout同一个tag,所有模块的代码版本天然对齐,不会出现“clang是昨天的,llvm是上周的,然后IR接口对不上”这种低智错误。

这种管理方式看似“只是把多个目录放在一起”,实际是整个项目能保持几十个小组并行开发的核心制度保障。

2. 学习llvm-project的最好起点:IR和Pass优化链路

2.1 LLVM IR的三种形态,先看可读文本

LLVM的核心抽象是IR,也就是中间表示。IR不是一种形态,而是三种:

  • 内存表示:编译器运行过程中,IR以C++对象图的形式存在于内存里。
  • bitcode:一种紧凑的二进制序列化格式,后缀通常是.bc。
  • 可读文本:也就是以.ll为后缀的汇编风格文本,人能直接看懂。

很多时候调试问题,面对的是.bc这种二进制,你会相当痛苦。所以我的习惯是随时用工具把二进制转成可读文本。比如:

clang -S -emit-llvm hello.c -o hello.ll

这样hello.c就会被编译成hello.ll,你能直观看到每一个函数是怎么被翻译成IR的。如果你手头已经有.bc文件,也可以用llvm-dis把它反向恢复成.ll。

IR的设计非常像“带类型的精简汇编”:每个寄存器只能是单赋值,变量以SSA形式存在,显式使用load/store操作内存。这种设计让后续的Pass分析非常舒服——因为没有复杂的“别名”和“生命周期”纠缠,每个def-use链都清楚得像是画在纸上的图。

2.2 Pass机制:从Legacy PM到New PM

Pass是LLVM优化的灵魂。一个Pass,就是对IR做一趟分析和变换的小模块,运行完一轮优化,可能消除了一些死代码,也可能把某段循环向量化了。

早期LLVM用所谓Legacy PassManager,每个Pass要继承某个Pass基类,靠全局注册器去声明自己。这种设计在项目小的时候很灵活,但后来Pass一多,全局状态碰撞、依赖顺序脆弱的问题越来越明显。

现在的新Pass管理器(New PM)改用PassBuilder和AnalysisManager这套体系,每个Pass显式声明自己需要哪些Analysis,由管理器统一调度缓存。这不仅仅是工程洁癖,而是真实场景下的稳定性要求——编译器一天要跑上百万次优化,任何顺序或缓存上的不一致都可能造成难以复现的bug。

如果你打算做自定义优化,一定要直接学新PM,官方已经逐渐淘汰Legacy了,没必要在旧体系上浪费时间。

2.3 用opt单步观察优化效果

想快速验证一个IR经过某个Pass后变成什么样子,不需要启动整个工具链,直接用opt就行。下面这个命令是查看hello.ll经过mem2reg(提升内存操作到寄存器)之后的结果:

opt -passes=mem2reg hello.ll -S -o hello.mem2reg.ll

-S表示输出可读文本,不加就是输出bitcode。我用这个命令最多的场景,是看某个Pass是否生效、是否引入了意料之外的指令变动。它比在Clang里加一堆优化flag精细得多。

优化前后对比还能用diff工具做文本比较,这种“眼见为实”的反馈,是我认为新手理解LLVM优化链路最快的方式。

3. 构建llvm-project:给新手的一份避坑操作手册

3.1 构建之前,先说清楚环境要求

很多人失败的根源不是不懂CMake,而是太低估这次构建的资源消耗。我自己第一次构建llvm-project,机器只有8GB内存,还开了默认的Debug模式加全量targets,结果直接把内存吃到swap,整个机子卡到鼠标都动不了。

所以在敲命令之前,请先跟自己的机器对一下配置:

  • 磁盘:源码加构建目录,建议预留至少50GB空闲空间,目录放在SSD上。
  • 内存:如果只是本地学习,16GB起步;之后要做全量测试,32GB更安心。
  • 构建工具:强烈建议用Ninja,别用Unix Makefiles。Ninja对并行任务的调度更高效,增量构建也快很多。
  • 目标架构:如果只需要X86,就不要贪心编译所有targe。

这里有个关键认知:Release模式构建产物更省内存和运行更快,但如果要调试LLVM自身,还是要开Debug加断言,这属于“性能换可调试性”的经典取舍。

3.2 从clone到构建完成,完整跑一遍

假设你从零开始,在llvm-project根目录下,推荐这样操作:

git clone https://github.com/llvm/llvm-project.git cd llvm-project mkdir build cd build cmake -G Ninja ../llvm \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld;mlir" \ -DLLVM_TARGETS_TO_BUILD="X86;AArch64;RISCV" \ -DLLVM_ENABLE_ASSERTIONS=ON ninja

该项目支持从源码构建出的构建系统放在独立的build目录,这样不会污染源码树。上面这个命令中,我选择了clang、lld、mlir三个常用的子项目,target也控制在了三个,避免全量编译地狱。

如果只想做一个能跑的clang,可以只写-DLLVM_ENABLE_PROJECTS="clang"。但如果要深入研究,建议至少把lld也带进来,因为链接环节是理解工具链完整流程的一环。

构建时间取决于机器,我曾在16核服务器上全量构建大约半小时到一小时,笔记本上可能要熬一个晚上。ninja在构建过程中会通过多核并行吃满CPU,这时最好不要同时开一堆重型应用。

3.3 我踩过的三个构建坑

第一,没有安装ninja和cmake新版。很多Linux发行版自带的cmake版本太老,LLVM的CMakeLists会直接报错。解决办法很简单,装好cmake 3.20以上版本,ninja用apt或brew都能装,Windows下则建议用Visual Studio的组件。

第二,源码目录路径别带中文或空格。某些工具链在阶段生成文件时会对路径做字符串拼接,一旦路径异构,各种奇怪错误都会冒出来,而且报错信息往往根本看不出和路径有关。

第三,不要把构建目录放在网络文件系统上。LLVM在构建过程中会创建大量临时文件,网络磁盘的I/O延迟会被无限放大,构建速度慢到离谱。我亲眼见过某位同事把build目录放在挂载盘上,跑了一个下午还没编完。

4. 用llvm-project做点实事:编译流程、自定义Pass与前端接入

4.1 从.c到可执行文件,每一步都在发生什么

很多人用gcc用了很久,却不清楚一个源码文件变成可执行文件时,内部到底经过哪些阶段。用LLVM工具链就能非常完整地拆开这个过程。

先写一个简单的hello.c:

#include <stdio.h> int main() { printf("hello llvm\n"); return 0; }

CLang的完整编译链路大致是:预处理、词法/语法分析、生成AST、生成IR、一系列Pass优化、指令选择、寄存器分配、生成目标汇编、汇编器转成目标文件、链接器链接成可执行文件。

我们可以用命令把它们拆开看:

clang -E hello.c -o hello.i # 只做预处理,展开宏和头文件 clang -S -emit-llvm hello.c -o hello.ll # 编译到LLVM IR文本 clang -c hello.c -o hello.o # 编译到目标文件 clang hello.o -o hello # 链接生成可执行文件

这里最有意思的是hello.ll。你能看到printf这个名字是如何被保留成外部声明,main函数的IR骨架又是如何搭建的。到了链接阶段,如果不用clang驱动ldd,也可以直接调用ld.lld来手动指定入口点、库路径,虽然那种做法在真实项目里很少见,但实验性质很强,能让你理解“工具链驱动”和“底层工具”之间的边界。

4.2 写一个最简单的自定义Pass,让它真正跑起来

纸上谈兵再多,不如动手写一个Pass。我推荐从函数级Pass入手,因为FunctionPass能对每个函数做遍历,效果直观,逻辑也不会太复杂。

新建MyPass.cpp:

#include "llvm/IR/Function.h" #include "llvm/IR/Instructions.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Support/raw_ostream.h" using namespace llvm; namespace { struct MyFunctionPass : public PassInfoMixin<MyFunctionPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { errs() << "Processing function: " << F.getName() << "\n"; for (auto &BB : F) for (auto &I : BB) errs() << " " << I << "\n"; return PreservedAnalyses::all(); } }; } // namespace extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "MyFunctionPass", LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "my-function-pass") { FPM.addPass(MyFunctionPass()); return true; } return false; }); }}; }

这个Pass做得很简单:遍历每个函数里的每条指令,然后打印出来。它的价值不在于优化,而在于让你跑通“插件注册 — opt加载 — 执行打印机”的全流程。

构建这个插件的命令:

clang++ -shared -fPIC -std=c++17 MyPass.cpp \ $(llvm-config --cxxflags --ldflags --libs) \ -o MyPass.so

这里需要确保llvm-config是你自己构建出的那个版本,而不是系统自带的旧版本。如果PATH不对,直接指定路径,例如build/bin/llvm-config。

然后准备一个测试IR文件:

define i32 @add(i32 %a, i32 %b) { entry: %sum = add i32 %a, %b ret i32 %sum }

保存为test.ll,接着运行:

opt -load-pass-plugin=./MyPass.so -passes=my-function-pass test.ll -S

如果成功,你会看到终端打印出“Processing function: add”,并且逐条输出IR指令。到这里,你的自定义Pass已经真实跑起来了。

4.3 前端层改造:除了Clang,还有很多更高层的玩法

很多人以为“用llvm-project”就等于“用Clang编译C/C++”。实际上前端层能做的事情远超这个范围。你可以给某个语言写一个自己的前端,把AST或高层中间表示降级到LLVM IR,然后白嫖后面几百个Pass和几十个后端。

比如你想发明一种新语言,只需要实现前端+翻译成IR,后面的寄存器分配、指令调度、目标代码生成全都不需要自己操心。这种“只做上游,不带下游”的开发模式,正是LLVM能吸引那么多语言实现者的原因。

MLIR则把这个理念又提升了一层:它允许你定义自己的dialect,即一套带有语义的IR方言,然后通过dialect之间的转换逐层lower到LLVM IR。AI编译器里很常见的做法是:框架导出计算图,在MLIR里做算子融合、内存规划、循环变换,最后降到LLVM的IR再交给普通优化器和后端。上面这一套流程的精髓,是把“高层数据流/控制流知识”复用到底层调度中,比直接从头做一套专用编译器省太多体力。

5. 问题排查与效率工具:把这些经验下沉到日常开发

5.1 常见错误速查表

我平时在群里帮人看LLVM相关报错,发现有一批问题出现频率极高。整理成一个速查表,方便你直接对照:

场景典型报错常见原因解决思路
CMake配置The source directory does not contain a CMakeLists.txt源码路径写错,指向了build目录或错误子目录确保cmake命令里的源码路径是llvm-project下的llvm目录
编译阶段内存不足/卡死Debug模式+全量target导致内存峰值过高改用Release,收窄-DLLVM_TARGETS_TO_BUILD
运行optUnable to load pass pluginPass插件ABI版本不匹配确认opt和插件由同一个LLVM版本构建,至少API版本一致
链接阶段undefined reference to llvm::...CMake里漏了对应LLVM组件库用llvm-config --libs补齐链接库
运行时崩溃Assertion failedLLVM_ENABLE_ASSERTIONS=ON暴露了内部约束问题不要关断言来“碰运气”,去查具体Assert位置
构建缓慢增量编译像全量重来修改公共头文件导致大规模重编尽量少动公共IR头文件,分离实验代码到独立组件
命令找不到llvm-config: command not found环境变量PATH没包含build/binexport PATH=build/bin:$PATH

这张表是我在实际使用中最常翻的,建议收藏。尤其是ABI版本不匹配,真的很烦人,每次升级LLVM版本都容易出现,只能老老实实把opt和插件放同一套build里跑。

5.2 调试LLVM自身的经验

当你开始改动LLVM源码,或者追查某个优化Pass的输出是否正确时,普通print大法会显得很低效。我的做法是:先在函数入口加errs()输出,确认该Pass是否被执行;如果找到了可疑代码区域,再用gdb/lldb打断点,查看IR对象的结构和值。

另外,LLVM社区自带一套基于lit的测试框架。你写的Pass如果希望形成长期回归保护,可以在test目录里写一个.ll测试文件,里面有RUN注释,比如:

; RUN: opt -load-pass-plugin=... -passes=my-function-pass < %s -S | FileCheck %s ; CHECK: Processing function: add

这样跑llvm-lit就能自动化验证Pass行为。这个过程对个人项目来说可能有点重,但一旦做了,后面回归效率提升是肉眼可见的。

我发现很多开发者没有充分利用update_test_checks.py这类工具,其实它可以根据当前工具的输出自动生成CHECK行,省去手工编写了大把时间。这个习惯值得养成。

5.3 我建议你收藏的几条习惯

第一,遇到“优化结果不对”的问题,永远先看IR,别急着看汇编。IR是前端和后端之间唯一确定的契约,删除中间转换的变量往往能更快定位问题源头。

第二,搭建一个最小复现用例。虽然LLVM庞大的项目结构让人想一口气跑整套工具链,但多数bug只需要一个很小.ll文件加一个opt命令即可复现。把问题缩小到单文件、单个Pass,调试体验会大幅提升。

第三,构建时把LLVM_ENABLE_ASSERTIONS打开。有人觉得断言拖慢性能,但在开发调试场景下,位置准确的assert比任何日志都好用。它会在IR约束不满足的第一时间爆出来,避免错误在后续几步被“掩盖”成更奇怪的现象。

6. 最后,一点私人心得

llvm-project这个项目,真正值钱的地方不是某个算法多精妙,而是它把“如何组织一个超大规模的基础软件工程”这件事摆在了你面前。我学习它最大的体会是:不要一上来就去读那本厚厚的编译器教科书,先把工具链跑通,再对照一个真实IR的变化去理解每个概念,效率会高得多。

建议你拿到这个项目后,挑一个自己最常用的操作场景,比如“把一段C++代码编译成可执行文件”,然后手动替换其中的一个Pass,加一行打印。只要走通这个流程,LLVM那些庞大复杂的源码树对你来说就不再是迷宫了,它只是一套“你能随时动手改”的普通软件。这个项目值得你花上一整个周末去折腾。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 11:51:46

基于MATLAB的指纹图像增强与细节点检测全流程解析

简介&#xff1a;一套面向本科毕设与课程设计的指纹图像处理完整方案&#xff0c;基于MATLAB实现&#xff0c;聚焦脊线增强之后的脊线分割、脊线细化、细节点检测与细节点验证&#xff0c;覆盖了形态学处理、后处理去伪等关键环节&#xff0c;适合正在开展生物特征识别或模式识…

作者头像 李华
网站建设 2026/9/20 11:50:56

快手账号权重在线查询系统:Python Flask源码与接口设计详解

简介&#xff1a;快手在线查权重源码&#xff0c;配套查询接口&#xff0c;聚焦快手账号权重查询场景&#xff0c;面向快手运营者、数据分析爱好者以及有PHP基础的后台开发人员&#xff0c;可用于搭建私有权重查询工具或理解第三方接口的调用与解析方式。压缩包共35个文件&…

作者头像 李华
网站建设 2026/9/20 11:50:04

Android 15 强制 edge to edge 适配:EdgeUtils 封装与实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 11:45:46

2026年产品管理系统测评:从六维模型到选型避坑实操指南

产品管理系统这个品类&#xff0c;这几年是我见过变化最离谱的软件赛道之一。从最早大家只管“需求池能装多少条”&#xff0c;到后来拼看板、拼工时、拼报表&#xff0c;再到现在AI开始往需求描述和任务拆解里钻&#xff0c;整个市场几乎是一年一个玩法。2026年开年&#xff0…

作者头像 李华
网站建设 2026/9/20 11:43:38

VS Code Claude Code 插件跳过登录:本地 API 密钥直连配置指南

1. 为什么我要折腾这个插件VS Code 里用 Claude Code 插件的人大概都遇到过同一个场景&#xff1a;装好插件&#xff0c;打开面板&#xff0c;弹出一个登录框&#xff0c;要求你走一遍官方账号授权流程。对于已经有自己 API 密钥、或者在公司内网环境里根本连不上授权页面的开发…

作者头像 李华