ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LLVM编译基础设施详解:从构建到IR调试的实战指南

LLVM编译基础设施详解:从构建到IR调试的实战指南 如果你在跑 Linux 桌面环境或者服务器上的图形应用时打开过调试日志多半见过这样一行输出llvmpipe (LLVM 15.0.7, 256 bits)。我第一次看到它时还以为是某个显卡驱动的报错后来才明白这是 Mesa 里的软件渲染器在告诉你它正借助 LLVM 作为 JIT 后端把图形着色器实时编译成当前 CPU 能跑的机器码。那一瞬间我就意识到LLVM 早已不只是编译器工程师的玩具而是整个开源软件生态里绕不开的基础设施。这篇文章我想从项目标题 lllvm-project出发把 LLVM 这套东西系统地讲一遍。包括它的核心设计思路、源码仓库结构、如何从零构建、怎么读懂 IR以及我在实际编程和调试中踩过的坑。适合刚接触编译原理的学生、想把 Clang/LLVM 当作工具链使用的开发者也包括那些被 llvmpipe 日志勾起了好奇心、想搞清楚它背后原理的人。我会尽量用实际遇到的例子和可复现的命令来讲少讲虚的多讲能直接用的东西。1. LLVM 到底是什么从编译器使用者到源码阅读者1.1 那段 llvmpipe 日志背后的 LLVM先回到开头那行llvmpipe (LLVM 15.0.7, 256 bits)。llvmpipe 是 Mesa 项目里的一个软件渲染驱动它不依赖任何 GPU 硬件纯靠 CPU 把 OpenGL / Vulkan 的着色器跑出来。问题是着色器是一段类 C 的代码CPU 不能直接执行于是 llvmpipe 就把着色器转换成 LLVM 的中间表示再让 LLVM 在运行时把这些中间表示编译成当前机器的汇编指令。这就是 JITJust-In-Time编译的典型用法。那行日志里的LLVM 15.0.7就是 Mesa 编译时链接的 LLVM 版本256 bits多指代码生成时启用了 AVX2 这类 256 位 SIMD 指令路径让 CPU 一次能处理更多数据。所以哪怕你机器上没有独立显卡只要有 LLVM软件渲染依然能跑得动很多图形程序。这也是为什么 CI 环境、虚拟机里经常看到 llvmpipe 的原因。从这里可以看出LLVM 的核心能力不只是把 C 语言翻译成汇编这么简单它更是一个可编程、可嵌入的编译组件库。llvmpipe 只是它的一个下游应用场景。1.2 一个名字引发的误会LLVM 全称 Low Level Virtual Machine底层虚拟机。我第一次看到这个名字也以为是类似 JVM 那样的虚拟机能跑字节码。实际上LLVM 从一开始设计的就不是传统意义上的虚拟机而是一套编译基础设施。它的核心是一套被称为 LLVM IR中间表示的代码形式配合围绕 IR 做分析、优化、代码生成的库函数。这个古老的名字来自 2000 年 UIUC 的一个研究项目后来项目范围越来越大名字带来的误会也越来越多。2003 年左右官方就意识到了这个问题于是对外更多的直接把整个项目统称为 LLVM而虚拟指令集这个含义反而被逐渐淡化。也就是说你想真正理解 LLVM不能只盯着编译器两个字要把它想成一套积木前端把各种语言翻译成 IR优化器改进 IR后端根据不同的 CPU 架构把 IR 变成机器码。这套积木既能组合成完整的编译器Clang也能被渲染器、静态分析器、脚本语言运行时等各种各样的软件嵌入使用。1.3 为什么说 LLVM 是编译基础设施基础设施这个词听起来有点空我举几个具体例子你就明白了。第一苹果从 GCC 切到 LLVM 后Xcode 的编译速度和错误提示质量都有了明显变化。Clang 对源码错误的定位比 GCC 精确很多诊断信息对普通开发者更友好这件事直接推动了大量开发者迁移。第二Rust 语言早期直接采用了 LLVM 作为官方后端Rust 编译器负责把 Rust 代码变成 LLVM IR剩下的优化、寄存器分配、指令选择全交给 LLVM。Swift 同样如此还有 Julia、Ruby 的 MJIT、甚至 GPU 生态里的 CUDA 编译器也大量吸收 LLVM 的技术。第三Android 的 RenderScript已废弃、各种数据库的向量化执行引擎也都在用 LLVM 做运行时编译。可以说现在做编译、虚拟机、程序分析、代码安全审计的人很难绕开 LLVM 这套东西。它已经超出了一个编译器的边界变成了整个软件产业底层的一块通用积木。2. 三阶段架构LLVM 最核心的设计思路2.1 传统编译器与三阶段模型的区别学过编译原理的人都知道传统编译器通常分成前端词法分析、语法分析、语义分析、生成中间代码、优化器各种优化 pass、后端代码生成、寄存器分配、指令调度。GCC 内部的架构也类似但它早期的中间表示与具体语言、具体目标平台的耦合比较紧密跨语言、跨平台的抽象做得不如 LLVM 彻底。LLVM 的经典三阶段架构可以简单概括成前端如 Clang负责把 C/C/Objective-C/OpenCL 等语言翻译成 LLVM IR优化器opt 工具干的活对 IR 做一系列不依赖具体 CPU 的分析和变换后端llc 工具加上 LLVM 各目标架构相关的代码把优化后的 IR 翻译成目标平台的汇编代码。这样做的好处很直白前端的语言特异性和后端的硬件特异性被隔离开了。你写一个优化 pass针对的是 IR不需要关心输入语言是 C 还是 Rust也不需要关心目标 CPU 是 x86 还是 ARM。反过来说新的语言接入 LLVM 生态时只需要写一个能产出 IR 的前端就能免费获得所有优化和后端生成能力。我在实际工作中最大的感受是这种分层让调试粒度变得非常清晰。一个编译问题你可以先用clang -emit-llvm看 IR 长得对不对如果 IR 是对的问题就在优化器或后端如果 IR 已经错了问题八成在前端语义处理上。这种通过中间层定位问题的能力在 GCC 体系里远没有这么顺手。2.2 为什么 IR 是整个架构的通用语这里的关键在于 LLVM IR 的设计目标它必须足够低级让优化器和后端能精确控制指令选择、寄存器分配等底层操作同时又要保留足够的类型信息和数据流信息让各种高级优化能安全执行。LLVM IR 是一种静态单赋值形式SSAStatic Single Assignment的表示。通俗点说SSA 要求每个变量只被赋值一次每次使用都能直接追溯到定义它的那条指令。这么做的最大好处是数据流分析变得非常简单——每个变量只有一个定义点编译器要查这个值是哪儿来的时不用在庞大的程序里到处找赋值语句。如果你非要让我用生活类比可以把 IR 想成电影拍摄时使用的数字中间片摄影机拍出来的原始素材源码不能直接放映得经过调色、剪辑、特效优化之后才能分发到各种放映设备不同 CPU 指令集上。中间片格式定得越好后期处理和分发到不同平台就越方便。LLVM IR 就是这套流程里那个被精心设计的中间格式而各种 pass 就是调色师、剪辑师、特效师。正因为 IR 是通用语LLVM 里的很多技术才可能被跨界复用你可以用 Clang 把 C 编译到 IR然后用 MLIR 生态的工具去做更高层次的变换你也可以直接从 Python 这样的脚本语言生成 LLVM IR进而利用后端的优化能力。这就是基础设施的意义所在。3. llvm-project 仓库结构与核心组件地图3.1 一眼看懂各个子项目用 git 克隆 LLVM 官方仓库时你得到的目录名就是llvm-project。这个仓库不是一个单一的编译器而是一个多项目集合。我第一次进去的时候也被它吓了一跳怎么这么多文件夹我把最核心的几个列出来对照表格看就很清楚了。子项目作用典型用途llvm核心库与工具包括 IR、优化器、目标后端、llc/opt/llvm-config 等所有子项目的基础clangC/C/Objective-C 前端提供 clang/clang 命令日常编译 C/C 代码clang-tools-extra附加工具如 clang-tidy、clang-format、clangd静态分析、代码格式化、IDE 支持lld高性能链接器替代系统默认 ld链接速度快很多lldb基于 LLVM 的调试器调试 C/C/Rust 程序支持表达式求值compiler-rt运行时库包括 Sanitizer、内置函数、profile 支持内存检查、未定义行为检测libc / libcabiC 标准库实现使用 Clang 时的 C 运行时libunwind栈回溯与异常处理相关库配合 libc 使用mlir多级 IR 基础设施面向机器学习编译等领域做新编译器框架的人经常用flangFortran 前端科学计算领域 Fortran 编译polly基于多面体模型的循环优化高性能计算优化实验如果你刚开始接触别被这么多名字吓到。大多数场景下你真正需要的是clang、llvm核心工具、lld其他的按需开启。3.2 日常使用中最常碰到的几个组件我在实际开发中最常用的几个组件是clang提供编译驱动。你可以直接clang hello.c -o hello它内部会调用 cc1 前端做真正的编译再做汇编、链接。打开-v参数能看到 clang 调用的详细子过程这点对排查问题很有用。llc是后端工具负责把优化过的 IR 生成汇编。写编译器的同学经常用它来验证某个 IR 输入在当前架构上的输出。opt是优化器工具按你指定的 pass 顺序处理 IR。lli是 IR 解释器/JIT 执行器可以直接运行 .bc 或 .ll 文件方便快速验证。llvm-config是开发 LLVM 相关工具时的瑞士军刀。它输出编译链接 LLVM 库需要的参数比如llvm-config --cxxflags --ldflags --libs core。写 CMake 时我也经常通过llvm-config --cmakedir来找 LLVM 的 CMake 模块路径。还有FileCheck和lit它们经常一起用在 LLVM 的测试框架里。我后面单开一节讲调试与测试时再细说。4. 从源码构建 LLVM一次完整的实战记录4.1 构建前准备与版本选择很多人一上来就git clone整个 llvm-project然后直接进目录构建结果等了几小时或者爆了磁盘就放弃了。我建议先想清楚三件事要哪个版本、要哪些组件、构建类型是什么。版本选择上如果你不是要做 LLVM 自身开发没必要追最新 main。我习惯选择一个稳定的 release tag比如llvmorg-15.0.7。这个版本对应你看到的 llvmpipe 日志版本社区反馈比较成熟。当然今天的主流版本已经到 18、19 甚至更新了选 tag 的原则是能用、稳、资料多。建议只启用你真正需要的组件。-DLLVM_ENABLE_PROJECTSclang;lld;clang-tools-extra这种写法意味着在 llvm 核心之外额外构建 clang、lld、clang-tools-extra。如果你是第一次构建先少开几个别把 mlir、flang 一起勾上否则编译时间会呈指数级上升。4.2 cmake 配置参数逐项说明我从官方文档和自己实践里整理了一份比较通用的配置你可以在llvm-project目录下建一个build目录然后执行下面的 cmakecmake -G Ninja -S llvm -B build \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;clang-tools-extra \ -DLLVM_TARGETS_TO_BUILDX86;AArch64;RISCV \ -DLLVM_ENABLE_ASSERTIONSON \ -DLLVM_USE_LINKERlld \ -DLLVM_PARALLEL_LINK_JOBS2 \ -DBUILD_SHARED_LIBSON逐项解释一下。CMAKE_BUILD_TYPERelease是必须的默认空配置等同 Debug编译出来的库非常大而且速度很慢。LLVM_ENABLE_PROJECTS控制额外要构建的子项目项目名之间用分号分隔。LLVM_TARGETS_TO_BUILD控制后端目标架构不写的话默认会构建所有支持的后端构建时间和磁盘占用都会显著上升。你只需要本机 X86再加一两个关心的架构就够了。LLVM_ENABLE_ASSERTIONS我建议开 ON。它会在 LLVM 内部加入大量断言检查对开发调试有极大帮助。代价是生成的编译器跑得稍慢一点但作为日常开发完全能接受。发布打包时再关掉即可。LLVM_USE_LINKERlld是一个很实用的小优化用 lld 来链接 LLVM 自身链接速度比系统默认的 GNU ld 快很多。这里涉及到一个先有鸡还是先有蛋的问题——你系统里必须已经有一个可用的 lld或者用系统包管理器装好lld否则构建会在链接阶段卡很久。LLVM_PARALLEL_LINK_JOBS2是为了防止链接期间内存耗尽链接器非常吃内存限制并行链接任务数是个稳妥保底手段。BUILD_SHARED_LIBSON会让 LLVM 库编译成动态库。优点是链接和体积更好控制缺点是有时候动态库版本匹配比较头疼。如果你是做 LLVM 工具开发、经常要改库代码建议开如果是打包部署更倾向静态库 OFF。4.3 构建过程中的资源控制与耗时预估配置完成之后进入构建目录执行ninja就可以开始构建。构建时间取决于你的 CPU 核心数和配置。我的一个经验8 核机器Release 模式只构建 clang 和 lld大概 20 到 40 分钟如果开 Debug 模式时间翻倍。如果你的机器内存只有 8GB建议不要开太多并行任务ninja -j4比ninja -j8更稳。磁盘方面llvm-project 完整构建不加特殊裁剪的话50GB 空间不一定够所以磁盘都要预留好。-DLLVM_TARGETS_TO_BUILD的裁剪能大幅减少磁盘占用。构建完成后建议先跑几个自检测试确认环境没问题ninja check-llvm ninja check-clang这两个命令会运行 LLVM 和 Clang 的测试套件如果全部通过说明你构建出来的工具链基本是健康的。这一步非常重要很多后续问题都源于构建配置不对或者环境不符。5. 用一条完整链路看透编译器的工作流程5.1 从 clang 到 IR看前端如何翻译源码构建好之后写一个最简单的 C 程序验证三阶段流程// hello.c #include stdio.h int add(int a, int b) { return a b; } int main(void) { printf(%d\n, add(2, 3)); return 0; }执行下面命令生成文本形式的 IRclang -O0 -emit-llvm -S hello.c -o hello.ll打开hello.ll你会看到 add 函数被翻译成了类似下面的样子define i32 add(i32 %a, i32 %b) { entry: %add add i32 %a, %b ret i32 %add }这就是 LLVM IR 的文本表示。i32表示 32 位整数add是全局符号名%a%b是局部 SSA 值。注意这里还没有任何 x86 或 ARM 的指令它只是一套独立于硬件的抽象指令集。这就是前端的作用把 C 语言的语义翻译成一份与具体 CPU 无关的中间表示。此时你还能看到函数签名、类型信息、变量名字这些信息对后续优化和后端都非常有用。5.2 用 opt 优化 IR理解中间层存在的意义有了 IR 之后接下来交给优化器。opt工具可以执行一系列优化 pass。看一个最简单的例子opt -S -passesmem2reg hello.ll -o hello_m2r.llmem2reg是一个非常经典的 pass它会把分配在栈上但只被简单读写的变量提升为 SSA 寄存器形式的虚拟寄存器。如果你用-O0生成 IR很多局部变量还是以alloca load/store 的形式存在经过 mem2reg 后代码会干净很多。这也是 clang 在开启优化时默认要做的一步。opt -O2等价于运行默认的 O2 优化序列里面包含了几十个 pass内联、常量传播、循环不变量外提、向量化等等。你不需要全部记住名字但要知道它是编译器里最耗也最关键的一环。在hello.ll上执行opt -S -O2 hello.ll -o hello_opt.ll你会发现add函数可能被内联进main因为函数足够小内联是显而易见的好处。同时常量折叠可能已经算出结果。这能直观告诉你优化器干了什么。5.3 用 llc 生成目标代码理解后端的作用最后一步把优化后的 IR 变成目标机器的汇编llc hello_opt.ll -o hello.sllc默认面向当前机器的 CPU生成的汇编文件你就可以直接用 clang 汇编并链接出可执行文件clang hello.s -o hello ./hello如果你指定-marcharm64比如交叉编译到 ARM64那生成的就是 ARM64 汇编。这就是跨平台编译的来源之一——IR 本身不绑定硬件后端按目标架构做指令选择、寄存器分配、指令调度即可。我自己在调试 IR 的时候经常用llc -print-after-all来打印每一条指令处理之后的 IR 状态它能迅速定位问题出在哪个 pass 上。这个在后面排查问题部分还要细说。6. 一篇文章读懂 LLVM IR6.1 三种表示形式LLVM IR 有三种表示形式内存中的表示、Bitcode、文本表示。它们只是同一信息的不同编码方式。内存表示是编译器在内存里直接操作的数据结构用来做分析和优化。Bitcode 是一种二进制格式扩展名通常是.bc适合存储、传输、JIT 加载它比文本更紧凑。文本表示才是我们在.ll文件里看到的可读格式方便调试和阅读。在命令行里你经常看到-S表示输出文本汇编实际是 IR 文本不带-S而带-c之类时输出的可能是 Bitcode。比如clang -emit-llvm -S hello.c -o hello.ll # 文本 IR clang -emit-llvm -c hello.c -o hello.bc # Bitcode两种格式可以用工具互相转换。llvm-as把文本转成 bitcodellvm-dis把 bitcode 转成文本。你前端自己生成 IR 时如果要交给 LLVM 处理走 bitcode 会更高效。6.2 IR 的层次结构与 SSAIR 的顶层单元是 Module你可以把它理解成一个编译单元里面包含了若干全局变量和函数。每个函数包含若干基本块BasicBlock每个基本块是一串顺序执行的指令最后一条指令必须是一个终结指令terminator比如 br、ret、switch、unreachable。基本块之间通过跳转指令形成控制流图CFG。SSA 是 IR 的一个核心特性。程序里每个虚拟寄存器以%开头的名字在静态代码中只被定义一次。这看起来很不方便比如循环里的变量多次赋值怎么办LLVM IR 用 phi 指令来解决phi 根据控制流选择从哪个入边取值。phi 指令初看很难懂但它是很多编译优化的基石。也可以把 SSA 想成白板上的标记每个值一旦写下就固定不变任何人都能看到它的定义当你需要把新的值带进循环时就划一条新的路径并在路径汇合处用 phi 指明如果从 A 来取 X如果从 B 来取 Y。这让数据流分析变得像看电路图一样清晰。6.3 类型系统与指令集IR 有一套简单的强类型系统。基本类型有i32、i64、float、double、ptr等聚合类型有数组[4 x i32]、结构体%struct.Foo type { i32, ptr }函数类型也有明确的签名。这套类型信息让 IR 能支持类型安全的优化比如 alias analysis 能利用类型信息判断两个指针是否可能指向同一块内存。IR 的指令集数量并不算多常见的有算术类add、sub、mul、sdiv、udiv、fadd、fmul等访存类load、store、alloca控制流类br、ret、switch、indirectbr、call、invoke转换类trunc、zext、sext、bitcast、ptrtoint等。相比 x86 汇编几百条指令IR 指令集相当克制。我在给新人讲 IR 的时候最喜欢举一个例子gep指令GetElementPtr。它用来计算结构体或数组元素的地址经常被初学者认为是一个移动指针的指令。实际上它只做地址计算不访问内存也不改变任何指针值。理解gep之后对指针类型和地址计算的理解会上一个台阶。你要是连gep都能讲清楚说明对 IR 的掌握已经很扎实了。7. 避坑指南LLVM 开发与调试常见问题7.1 构建配置类问题我见过最多的失败案例几乎都是构建配置不当导致的。第一个坑是 Debug 模式健忘症。有人没指定CMAKE_BUILD_TYPERelease用默认 Debug 构建然后开始抱怨LLVM 怎么这么慢、这么大。Debug 模式下的编译器带有大量断言和未优化的代码跑起来慢是正常的不是 LLVM 本身的问题。第二个坑是全 target 构建。默认LLVM_TARGETS_TO_BUILD是 all会把 X86、ARM、AArch64、Mips、PowerPC、RISCV、WebAssembly 等几十个后端全编一遍。磁盘占用和构建时间都会增加好几十G。构建之前先在 CMake 里指定自己需要的 target 列表节省掉的时间足够你泡好几杯咖啡。第三个坑是内存不足。链接libLLVM时多个链接任务并行会导致内存飙到十几 GB。用LLVM_PARALLEL_LINK_JOBS2限制并行链接任务数或者改用lld都能缓解。同样的问题出现在ninja -j数值过大的时候核心攒不够就起了很多编译任务编译进程本身还好链接阶段才是内存杀手。7.2 API 与版本变迁类问题LLVM 是一个迭代非常快的项目API 变化频繁。我给三个留心点第一新/旧 Pass Manager 的写法。LLVM 14 之后默认走新 Pass Manageropt的-passesxxx参数是新式写法旧的-xxx形式在新版本里很多已不可用。我写的示例opt -S -passesmem2reg就是新式写法不要再用opt -mem2reg。第二头文件的组织方式。LLVM 14/15 之后经历过一轮头文件目录重组以前常见的#include llvm/IR/Function.h有些情况下被拆分或移动。你要是从网上搜到老代码经常需要改成新的头文件路径。建议以你实际安装版本的include/llvm目录为准。第三llvm-config的坑。llvm-config --cxxflags输出的是编译 LLVM 自带工具时用的 flag其中可能包含-fno-exceptions和-fno-rtti。如果你的项目需要 RTTI 或异常处理直接把这些 flag 拷过来会导致编译失败。正确做法是参考 LLVM 官方文档中Using LLVM from CMake的llvm_map_components_to_libnames方式而不是手撸编译命令。7.3 调试 LLVM 的三件套调试 LLVM 相关的程序我有三件套几乎不离手断言、-print-after-all、以及 lit 测试。打开LLVM_ENABLE_ASSERTIONSON后很多内部不变量会在出错时立即触发 assert能帮你从源头抓住问题。如果关闭断言某些错误可能要等到程序跑飞或产生错误结果时才能发现定位成本高很多。opt -print-after-all会在每个 pass 之后打印出当前的 IR。我调试自己的自定义 pass 时最喜欢搭配一个小脚本抓出 pass 前后 IR diff一眼看出自己的变换是否正确。命令大致是opt -S -passesmy-pass -print-after-all hello.ll -o /dev/null 2 trace.log然后把 trace 文件里每个 pass 之前的 IR 片段和之后的片段对比。这个技巧我在官方文档里没看到过集中讲解但实际调试中极其好用。lit是 LLVM 的测试驱动框架它扫描测试目录按照每个测试文件里的RUN指令执行命令再用CHECK模式匹配输出。你写新 pass 时建议直接把测试用例交到test/Transforms/YourPass/下用llvm-lit跑一遍比手动命令验证有保障得多。8. 从 LLVM 项目里还能学什么8.1 项目组织与代码风格很多人把 llvm-project 当作一个编译器使用说明书来看但我建议所有对 C 大型项目有兴趣的人都去读一读它的源码。LLVM 代码风格有极强的一致性命名空间简洁、类名和函数名风格统一、注释要点清晰。它对命名空间llvm内部的组织方式对大型代码库的模块划分很有参考价值。LLVM 读者目录里还有一份叫llvm/docs/CodingStandards.rst的文档讲代码风格、头文件包含顺序、错误处理模式等问题。即使你不打算给 LLVM 贡献代码这份文档也值得一读它代表了一种典型的 C 工程化规范。8.2 测试与验证体系LLVM 的测试体系可以当作软件工程教科书来拆解。它有一套非常庞大的测试目录每个后端、每个 pass、每个前端特性都有对应的测试用例。测试大多以 text-based 形式存在于.ll、.c、.s文件里配合 lit 和 FileCheck 使用。FileCheck 的匹配机制值得一提。它不像传统单元测试那样断言全文件输出而是在命令输出中搜索特定模式并且要求这些模式按顺序出现。这种设计对编译器这种输出长而杂的程序非常合适你不需要匹配整段汇编只要匹配能证明正确性的关键模式即可。比如要证明一个优化确实把乘法替换成移位你只写一个CHECK: shl就够无需精确到每个指令。8.3 如何参与社区贡献如果你想给 LLVM 提交 patch流程并没有想象中复杂。先从 GitHub 上找到对应子项目的 README了解贡献流程。一般先在邮件列表或者 GitHub Issue 里讨论设计然后 fork 仓库、创建分支、修改、跑测试最后提交 Pull Request。这里我给一个最诚恳的建议第一次贡献别选高大上的优化 pass从修 bug、补测试、改进文档开始。我自己从提交第一个小测试用例到现在深切体会到先熟悉流程再谈深度的效率。LLVM 社区的 review 非常严格行级别讨论经常深入到你觉得自己是个菜鸟的程度但正是这种较真保证了项目长期能用、可维护。写在最后的实际经验如果你准备开始玩 LLVM我给的最直接建议是先别急着读源码先动手。拿 clang 编一个已有项目、用 opt 调一两个 pass、用 llc 看看不同架构的汇编差异这些都是立刻能获得正反馈的路径。等你产生了这里为什么是这么实现的的疑问再打开源码读效率和动力都会高很多。我最初在 llvmpipe 日志里看到 LLVM 字样时完全没有料到后来会花那么长时间去读它的 IR 和 pass 机制。但回头来看这个被好奇驱动、顺着问题深挖的路径恰恰是最适合个体开发者融入 LLVM 生态的方式。希望这篇文章也能帮你少踩几个坑更早体会到这套编译基础设施的乐趣。
返回列表