从源码构建到自定义Pass:LLVM与Clang实践指南
2026/9/18 9:59:08 网站建设 项目流程

我第一次真正下载 llvm-project 源码,纯粹是因为一条 Clang 报错信息看不懂。当时手头有段 C++ 循环性能怎么也上不去,Clang 提示-Rpass-missed=loop-vectorize,我顺着这个选项去翻优化器到底在做什么,结果一头扎进了这个项目。坦白讲,第一次面对 llvm-project 时压力非常大——这个仓库的体量、编译时间、CMake 参数复杂度,都远超我此前接触过的任何开源项目。但随着整条工具链被我从源码构建出来,又亲手写出第一个 LLVM Pass,再回来看 IR 和 pass 之间的关系,很多之前的疑问都自然消失了。

这篇文章的定位,不是给做编译器内核开发的人讲源码细节,而是面向所有“想用 LLVM 生态做点实事”的开发者:无论是想基于 Clang 做静态分析或者代码定制,想给公司内部 DSL 做一个编译器后端,还是单纯想弄明白-O2到底对你的代码做了什么。我会从仓库结构、源码构建、IR 观察、自定义 Pass 到 MLIR,把这段实操经历完整写下来。你会看到不少我在真实构建过程中踩过的坑,这些坑往往不会出现在官方文档里。

1. 源码拉下来那一刻,你看到的是一个“编译器宇宙”

很多人第一次见到 llvm-project 的反应跟我一样:找个main()入口翻半天,结果发现根目录下没有传统意义上的“主程序”。这里压根不是一个项目,而是一个由多个子项目组成的 monorepo。理解这一点,比急着编译更重要。

1.1 为什么 monorepo 比零散仓库更合理

2019 年 LLVM 从 SVN 迁移到 GitHub monorepo,是一个非常关键的转折。更早的时候,LLVM、Clang、compiler-rt、LLDB 各有独立的 SVN 仓库,跨仓库协作要频繁同步版本,痛苦不堪。现在同一个仓库里能看到完整的“前端 -> 优化器 -> 后端 -> 链接器 -> 调试器 -> 运行时”链条,这是理解 llvm-project 的第一个关键认知。

拉下代码后,我建议你先别急着进llvm/目录,而是把根目录挨个看一遍。我简单列一下主要目录的职责:

目录职责我给新手的备注
llvm/核心库:IR、中端优化、CodeGen、Target、MC 等整个项目的发动机,C++ 写的,代码量大
clang/C/C++/Objective-C 语言前端把源码解析成 AST,再生成 LLVM IR
clang-tools-extra/clangd、clang-tidy、clang-query 等工具静态分析、IDE 相关功能都在这
lld/高性能链接器构建 LLVM 时自己用 lld 能省很多内存
lldb/调试器很多调试技巧和 LLDB 绑定都在这
compiler-rt/ASan、UBSan、TSan 等运行时库跑 sanitizer 时会用到
libc++/libc++abi/C++ 标准库实现及 ABI 层想“自己给自己造一套工具链”时是关键
mlir/多级中间表示编译器基础设施新编译器、加速器后端的必看方向
flang/Fortran 前端科学计算领域会用到
polly/多面体循环优化默认没启用,但很有意思
bolt/链接后二进制优化工具我目前只在 profiling 场景用到过
openmp/OpenMP 运行时和编译器支持并行编程相关

一开始不需要全部搞懂。我从llvm/clang/入手,因为绝大多数“编译器行为”的问题,最后都会落到这两个目录的代码上。

还有个容易忽略的点:Clang 其实不是 LLVM 的全部。LLVM 核心并不负责解析 C++,Clang 负责把源码解析成 AST,再生成 LLVM IR,之后才轮到 LLVM 核心优化和生成机器码。Clang 和 LLVM 的分工,是 llvm-project 设计里最经典的“前后端分离”。你在命令行里敲的clang++,更像一个“前端 + 驱动”,真正做优化和指令选择的是 LLVM 后端。

1.2 版本号、发布节奏与稳定分支怎么选

llvm-project 的版本节奏相当稳定,每年 3 月和 9 月左右各出一个大版本。如果你git checkout main,面对的是每天都在变的开发分支,一些 API 可能刚提交就被改掉了。做实验、写博客、搭工具链,我建议选稳定 tag。

我的选择是llvmorg-18.1.8。这一步很关键:

git clone https://github.com/llvm/llvm-project.git cd llvm-project git checkout llvmorg-18.1.8

你可以先用git tag -l "llvmorg-*" | tail -20看看有哪些 tag 可选。Release 版本都会提供 ReleaseNotes,想知道某个版本改了什么,直接看llvm/docs/ReleaseNotes.rst。如果你是通过系统包管理器装的 LLVM,版本号通常是对应发行版打包的版本,可能比上游落后一两个版本。做简单实验没问题,但如果要自己写 Pass 插件,建议还是源码构建,因为你需要与opt完全一致的头文件和构建配置。

2. 从零构建一次没有想象中简单:配置、内存和三个连环坑

网上关于“构建 LLVM”的教程不少,但大多数只给你一行cmake && make,完全不提后续的雷。我第一轮编译遇到的第一个问题不是报错,而是“内存不足”:链接阶段直接把我的 16GB 机器干到 OOM。这一节我会把构建命令、参数含义和真正值得注意的坑一起说清楚。

2.1 构建前的环境准备与 CMake 配置参数拆解

我推荐的环境:Linux 或者 WSL2,磁盘剩余 60GB 以上(越多越好),内存至少 16GB,如果条件允许 32GB 会更舒服。构建工具方面,Ninja 比 Make 快一截,所以一定要装 Ninja。

cmake -S llvm -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_TARGETS_TO_BUILD="X86" \ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld" \ -DLLVM_USE_LINKER=lld \ -DLLVM_ENABLE_PLUGINS=ON \ -DLLVM_PARALLEL_LINK_JOBS=1 ninja -C build clang lld

逐个解释这些参数:

  • -DCMAKE_BUILD_TYPE=Release:构建优化后的 Release 版本,速度和磁盘占用都比 Debug 好很多。Debug 构建的对象文件极大,普通硬盘很容易被撑爆。
  • -DLLVM_ENABLE_ASSERTIONS=ON:开启断言。推荐保留,因为许多 LLVM 内部 API 的约束靠断言检查,写 Pass 时能提前暴露问题。
  • -DLLVM_TARGETS_TO_BUILD="X86":只构建 X86 后端。默认情况下 LLVM 会构建所有后端,ARM、RISC-V、AArch64、Mips 等,时间长很多。如果你不是做交叉编译,只保留本机架构即可。
  • -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;lld":控制哪些子项目参与这次编译。注意不是所有组件都放这里,像 libc++ 这类运行时通常按“runtimes”方式构建,因为需要基于新编译出来的编译器再次构建,所以初学者别一开始就加 libc++。
  • -DLLVM_USE_LINKER=lld:用 lld 做链接器。这能大幅降低链接内存峰值,也快很多。但这要求系统里已经有一个 lld 可执行文件,或者你上一次已经构建出了build/bin/lld。我在第一次构建时没有系统 lld,所以第一轮先没加这个参数,只ninja lld,等 lld 出来后,再重新跑 cmake 加上-DLLVM_USE_LINKER=lld做增量构建。
  • -DLLVM_ENABLE_PLUGINS=ON:如果后面想写 Pass 插件并在运行期动态加载,这个必须打开。新手很容易漏掉这一步,结果编译插件时报“Plugin not loadable”。
  • -DLLVM_PARALLEL_LINK_JOBS=1:限制同时链接的任务数。Ninja 默认并行链接,多个大目标同时链接时内存会叠加,很容易触发 OOM。这个参数是我后来最常用、也最救命的一个。

还有一个容易被忽略的 Linux 坑:Ninja 并行任务多,如果ulimit -n文件描述符上限太低,编译时会报 “Too many open files”。解决办法是构建前先执行ulimit -n 4096,或者写进 shell 配置。

2.2 链接阶段内存爆炸与 lld 的救场

我第一轮构建,用的默认 ld.bfd,跑了很久之后在链接libLLVMclang的阶段直接 OOM。后来发现原因有两层:一是链接器本身内存吃得多,二是 Ninja 默认会同时启动多个链接任务,内存峰值是叠加的。

-DLLVM_PARALLEL_LINK_JOBS=1解决的是第二层问题;-DLLVM_USE_LINKER=lld解决的是第一层问题。lld 在链接大型 C++ 程序上的内存占用和耗时都明显低于 GNU ld。我实测同环境下,链接 clang 的峰值内存能下降三分之一以上,链接速度更是肉眼可见地快。

如果你只有 16GB 内存,至少配置一块 8GB 的 swap 来兜底。不过最稳妥的做法还是:只构建 X86 target、只构建 clang/lld/opt 这几个目标,不要一时兴起ninja install

2.3 减少构建量的两个关键开关

很多新手看网上教程全量编译,结果一个 Release 构建下来用了 80GB 磁盘。我当时踩了全量 target 的坑,后来总结下来,真正影响构建量/时间的是下面几项:

开关默认行为我的建议
LLVM_TARGETS_TO_BUILD全部 target只要不是交叉编译,设成"X86""X86;AArch64"
LLVM_ENABLE_PROJECTS看版本只加你需要的子项目
LLVM_BUILD_TOOLSON保留,optllcllvm-dis都靠它
LLVM_INCLUDE_TESTSON想跑测试就留 ON,只看代码可设 OFF
LLVM_INCLUDE_EXAMPLESON学习阶段可留,构建时间略增
LLVM_CCACHE_BUILDOFF有 ccache 建议 ON,后续迭代能快很多

另外,构建期间不要急着ninja install。直接使用build/bin/clangbuild/bin/opt完全没问题。install 一次会把整套工具复制到系统路径,磁盘占用直接翻倍,对初学者意义不大。

ninja -C build clang lld这种精确指定 target 的做法,比直接ninja最省资源。写 Pass 时只需要opt和一个clang,完全没必要全量构建。

2.4 自带测试集的作用:不只是验证正确性

ninja -C build check-llvm是 LLVM 每次大改后跑回归测试的命令。但很多人不知道,这套测试集也是绝佳的学习材料。测试文件里到处都是类似这样的模式:

; RUN: opt -passes=instcombine -S %s | FileCheck %s ; CHECK: ...

这是一种叫 FileCheck 的测试框架:RUN行描述要执行的命令行,CHECK行描述期望输出。我看 IR 优化、写自定义 Pass 的时候,经常去llvm/test/Transforms/下翻相关用例,这比直接看源码更能理解一个 pass 的行为边界。

跑单个测试文件也很简单:

build/bin/llvm-lit -v llvm/test/Transforms/InstCombine/add.ll

你可以把某个你关心的 pass 测试文件打开,对着 IR 和 CHECK 行看,很多“为什么优化器这样做”的答案,就藏在测试里。

3. 用 Clang 把 C++ 拆成 IR,我才真正看懂了优化器

构建完成后,第一个让我有“原来如此”感觉的操作,不是编译某个大型程序,而是把一段很简单的 C++ 代码转成 LLVM IR 来看。代码是经典数组求和:

// vec.cpp int sum_vec(const int *arr, unsigned long n) { int s = 0; for (unsigned long i = 0; i < n; ++i) s += arr[i]; return s; }

3.1 一条命令把 C++ 变成可读的 LLVM IR

build/bin/clang++ -O1 -S -emit-llvm vec.cpp -o vec.ll

打开vec.ll,你会看到类似这样的中间表示(经过精简):

define i32 @_Z7sum_vecPKm(ptr noundef %arr, i64 noundef %n) { entry: br label %for.cond for.cond: %i.0 = phi i64 [ 0, %entry ], [ %inc, %for.inc ] %s.0 = phi i32 [ 0, %entry ], [ %add, %for.inc ] %cmp = icmp ult i64 %i.0, %n br i1 %cmp, label %for.body, label %for.end for.body: %arrayidx = getelementptr inbounds i32, ptr %arr, i64 %i.0 %load = load i32, ptr %arrayidx, align 4 %add = add nsw i32 %load, %s.0 br label %for.inc for.inc: %inc = add i64 %i.0, 1 br label %for.cond for.end: ret i32 %s.0 }

这段 IR 对第一次看的人可能有点吓人,但拆开其实很好理解:

  • %开头的是虚拟寄存器,也就是 SSA 形式里的“值”。
  • phi指令用来解决循环变量的问题。%i.0在第一次循环时从entry块拿到 0,后续从%for.inc拿到自增后的值。你可以把 phi 理解成“接力棒交接点”:每个基本块进来时,根据从哪个前驱块跳过来选择上一轮的值。
  • getelementptr是 LLVM 里最容易被误解的指令。它不做内存访问,只做地址计算。getelementptr inbounds i32, ptr %arr, i64 %i.0表示在%arr基础上偏移%i.0i32元素,计算出一个新地址。
  • add nsw i32里的nsw是“no signed wrap”的缩写,意思是这条加法不会发生有符号整数溢出。有了这个标记,优化器才能安全地对整数运算做交换、分配律等变换。

源码里的 for 循环,在 IR 里变成了一堆基本块之间的跳转。循环变量si都变成了“每轮循环都会产生一个新值”的 SSA 值。这就是编译器优化依赖的数据流图:每条指令的依赖关系非常清晰,方便 pass 分析和变换。

3.2 optimization report:听懂优化器的“自言自语”

看懂了 IR,下一步就是听优化器怎么解释自己的决定。LLVM 提供了很好的诊断接口:

build/bin/clang++ -O2 -march=native \ -Rpass=loop-vectorize \ -Rpass-missed=loop-vectorize \ vec.cpp -o /dev/null

输出大概是这样:

vec.cpp:4:21: remark: vectorized loop (vectorization width: 4, interleaved count: 2) [-Rpass=loop-vectorize]

这句话信息量很大。vectorization width: 4表示循环每轮同时处理 4 个元素,interleaved count: 2表示生成了 2 份独立展开的循环体,相当于同时维护多条独立累加链,减少单条链的延迟等待。编译器之所以敢这么干,是因为它发现我们的s += arr[i]是纯粹的顺序累加,没有跨迭代依赖,适合用 SIMD 指令并行。

但你如果稍微改一下代码,让累加过程引入依赖,比如a[i] = a[i-1] + 1,循环就没有那么容易向量化了。这时-Rpass-missed会告诉你具体原因。这种“优化器自己报告为什么不做某件事”的能力,比任何优化书籍都直接。

还有一个实战技巧:如果你有一个数组输出参数,编译器会因为“无法证明 out 和 in 不重叠”而注入运行时别名检查,甚至放弃向量化。这时候写int *__restrict out能帮编译器确认别名关系,从而生成更激进的向量化代码。这种结论不是说出来的,而是-Rpass-analysis=loop-vectorize一行一行看诊断看出来的。

3.3 instcombine:理解优化器为什么“总在做小动作”

接下来可以体验另一个层级的优化:把 IR 丢给instcombine这个 pass 做等价化简。

build/bin/opt -S -passes=instcombine vec.ll -o vec.inst.ll

对比vec.llvec.inst.ll,你会发现很多简单模式被替换成更小的指令集合。比如x - x可能直接被优化成 0,x * 0直接被优化成 0。这个 pass 的核心理念是“每次只做很小的局部化简”,但组合起来就是-O2背后的强大力量。LLVM 的优化器不是一个巨大的黑盒,而是一堆小 pass 的流水线,每个 pass 只对自己负责的 IR 模式做变换。这是我在读llvm/lib/Transforms/InstCombine/时最有感受的一点。

写完这段,我已经能区分-O0-O1下 IR 的巨大差异了。-O0的 IR 里甚至连循环展开、常量传播都不做,所有变量都老老实实地allocaloadstore。当你真正看懂 IR 的变化,编译器优化的“神秘感”就消失了一大半。

4. 写一个自己的 FunctionPass 并跑起来:从老 PM 到新 PM

看懂了 IR,自然想动手改 IR。这里我给自己的目标是写一个非常简单的 FunctionPass:统计每个函数里有多少条二元运算指令,并打印出来。它不修改任何指令,安全性高,特别适合作为上手项目。

4.1 为什么直接上 New Pass Manager

LLVM 有老 Pass Manager 和新 Pass Manager 两套体系。老 PM 用virtual bool runOnFunction(Function &)这种形式,存在不少问题,比如 pass 之间共享分析结果困难、难以并行。新 PM 从 LLVM 14 开始成为默认,核心设计是:

  • pass 通过PassInfoMixin定义,不需要继承一堆虚函数。
  • 分析结果由AnalysisManager统一管理和缓存。
  • pass 声明自己PreservedAnalyses,让优化器知道哪些分析结果还可以复用。

所以新写的代码、新写的插件,都应该围绕新 PM。我的 pass 对应新 PM 的FunctionPass形态:输入一个Function,遍历它的基本块和指令。返回PreservedAnalyses::all()表示“我没有修改任何 IR”,这样优化器可以保留已有的分析结果。

4.2 Pass 插件骨架与注册入口

代码放在MyPass.cpp里,完整内容如下:

#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/StringRef.h" #include "llvm/IR/BasicBlock.h" #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 CountInstPass : public PassInfoMixin<CountInstPass> { PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { unsigned binaryOps = 0; for (BasicBlock &BB : F) { for (Instruction &I : BB) { if (isa<BinaryOperator>(&I)) { ++binaryOps; } } } errs() << "[MyPass] " << F.getName() << " has " << binaryOps << " binary operators\n"; return PreservedAnalyses::all(); } }; } // namespace extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return { LLVM_PLUGIN_API_VERSION, "MyPass", LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "count-inst") { FPM.addPass(CountInstPass()); return true; } return false; }); }}; }

这段代码有四个关键点:

  1. extern "C" LLVM_ATTRIBUTE_WEAK导出的llvmGetPassPluginInfo是插件与opt之间的“握手入口”。opt在加载共享库时,会去找这个符号,拿到插件版本信息、插件名和注册回调。
  2. LLVM_PLUGIN_API_VERSION是 API 兼容性检查。如果插件和opt版本不匹配,加载会失败。这就是为什么必须用同一个 llvm-project 构建出的opt来加载你自己的插件。
  3. registerPipelineParsingCallback的作用,是告诉 PassBuilder:当你遇到名字等于count-inst的 pass 时,把这个类加入 FunctionPassManager。这就是opt -passes=count-inst能被识别的原因。
  4. PreservedAnalyses::all()表明当前 pass 没有修改任何 IR,这是新手最容易漏掉的地方。如果改了 IR 却依然返回all(),优化器会误以为分析结果仍然有效,可能产生错误的后续优化。

4.3 用 CMake 构建插件,并让它在 opt 里跑起来

插件不能直接编译成可执行文件,要编译成共享库。最简单的 CMake 配置如下:

cmake_minimum_required(VERSION 3.20) project(MyPass CXX) find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") add_library(MyPass MODULE MyPass.cpp) set_target_properties(MyPass PROPERTIES CXX_STANDARD 17 CXX_EXTENSIONS OFF) target_include_directories(MyPass PRIVATE ${LLVM_INCLUDE_DIRS}) target_compile_definitions(MyPass PRIVATE ${LLVM_DEFINITIONS}) target_link_libraries(MyPass PRIVATE LLVMSupport LLVMCore LLVMPasses)

构建命令:

cd /path/to/my-pass cmake -S . -B build -DLLVM_DIR=/path/to/llvm-project/build/lib/cmake/llvm cmake --build build

LLVM_DIR要指向你之前构建 llvm-project 时生成的 CMake 配置文件目录,通常在build/lib/cmake/llvm。如果找不到,检查一下第一次配置时有没有把 LLVM 的 cmake 文件生成出来,一般构建过程中会自动生成。

测试插件:

/path/to/llvm-project/build/bin/clang++ -O1 -S -emit-llvm vec.cpp -o vec.ll /path/to/llvm-project/build/bin/opt \ -load-pass-plugin=/path/to/my-pass/build/MyPass.so \ -passes=count-inst \ vec.ll -S -o /dev/null

预期输出:

[MyPass] _Z7sum_vecPKm has 2 binary operators

函数名_Z7sum_vecPKm是 C++ 的名称修饰(mangling)结果,不用被它吓到。2 个二元运算指令对应的就是 IR 里的add i32add i64

如果你想在 clang 编译真实源码时也跑这个 pass,光靠-fpass-plugin是不够的,因为 clang 的默认编译流水线不会自动调用count-inst。你需要把 pass 注册进 PassBuilder 的 pipeline 扩展点,例如:

PB.registerPipelineStartEPCallback( [](ModulePassManager &MPM, OptimizationLevel OL) { MPM.addPass(createModuleToFunctionPassAdaptor(CountInstPass())); });

然后用:

/path/to/llvm-project/build/bin/clang++ \ -fpass-plugin=/path/to/my-pass/build/MyPass.so \ vec.cpp -O2 -c -o vec.o

不过对新手来说,用opt做 pass 验证是最直接、最快的方式。等 pass 逻辑稳定了,再考虑接入 clang 的完整流水线。

这里提醒一个非常容易踩的坑:插件共享库不能使用系统自带的 clang/opt 来加载。必须使用同一个 llvm-project 构建出的opt和头文件版本。版本不一致时,LLVM_PLUGIN_API_VERSION会直接报错;就算没报错,运行时也可能因为 ABI 不兼容而崩溃。

5. MLIR 才是 llvm-project 里最值得持续盯着的部分

如果停留在“写一个 FunctionPass”这里,你对 llvm-project 的理解还是停留在“传统编译器”层面。真正让这个项目比“LLVM 单仓库”更有想象力的,是mlir/目录。我在第一次看到 MLIR 时完全懵住了,过了很久才理解它想解决的问题。

5.1 从 MLIR 看 LLVM 生态的扩容

传统编译器路径通常是“前端 -> IR -> 后端”。LLVM IR 是非常不错的中端表示,但它仍然站在“高级语言”和“机器码”之间,它的设计目标是通用 CPU 指令集。可现实中还有很多目标并不太适合直接用 LLVM IR 表达:GPU、TPU、FPGA、DSP,以及 TensorFlow/PyTorch 这类框架里的计算图。它们需要不同层级的抽象和优化。

MLIR 的核心思路,是让用户可以定义自己的“方言”(dialect),每一种方言都是一套自定义的 IR 结构。你可以先在高层次用数学运算方言描述计算,然后逐步下降到更接近硬件的方言,最后再降到 LLVM IR,由 LLVM 后端完成最终机器码生成。这个过程在 MLIR 里叫“分层下降”(gradual lowering)。

我打个比方:LLVM IR 像是“给一位资深工人看的车间装配图”,而 MLIR 则允许你从“产品设计蓝图”开始,一层层细化成“结构图”“零件图”“装配说明”,每一层都有对应的优化手段。不同的框架和硬件厂商可以共用同一套编译基础设施,但各自保留自己的抽象层次。

这也是为什么 LLVM 项目要把 MLIR 放进同一个 monorepo:MLIR 的默认下降终点就是 LLVM IR,它大量依赖 LLVM 的 pass 基础设施和方言转换框架。放在同一个仓库里,可以保证接口同步,不会出现“MLIR 更新了,但 LLVM 的另一半 API 变了”这种断裂。

5.2 适合普通开发者的侧向切入点

很多人听我说 MLIR,第一反应是“这是编译器博士才需要学的东西”。其实不然。普通开发者想接触 MLIR,有一个很友好的官方教程:mlir/examples/toy/。它会带你用 MLIR 实现一个叫 Toy 的小语言,从定义 AST、生成方言、做方言转换,到最后降到 LLVM IR,整个过程非常完整。我花了一个周末读完 Chapter 1 到 Chapter 7,最大的收获不是会写 MLIR,而是看懂了“编译器如何为客户领域定制一套 IR,再嫁接到通用后端”。

你也可以在自己构建好的构建目录里试运行mlir-opt

build/bin/mlir-opt --help

看看--convert-linalg-to-loops这类转换 pass 的名字,能直观感受到“方言下降”的存在。它们和 LLVM 传统 pass 一样,也是可以用opt类似方式加载和调试的。

我的个人体会是,学习 llvm-project 的顺序其实有点反直觉。很多人一上来就啃 MLIR,结果被各种 dialect 术语劝退。更稳妥的路径是:先把 Clang 到 LLVM IR 这条路走通,写一个传统 FunctionPass,理解 pass 基础设施,然后再进 MLIR。因为 MLIR 的很多概念,比如 pass 流水线、IR 结构、分析管理,都延续自 LLVM 核心,基本功放在哪个层次都适用。

构建 llvm-project 确实会消耗掉一个周末,但它不只给我留下了一堆可执行文件。现在我调试编译问题、写代码生成工具,或者读别的开源编译器项目时,脑袋里会有一张完整的“源码 -> IR -> pass -> 机器码”地图。如果你也是被某个-Rpass报错吸引来看 llvm-project,我建议你按这篇文章的顺序走一遍:先构建,再拆 IR,最后让一个自定义 pass 真正跑起来。整个过程里最能让你“啊哈”一下的,一定是第一次看到自己的 pass 在 IR 上打出那行输出。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询