ARTICLE DETAIL

资讯详情

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

LLVM项目深度解析:从三段式架构到llvmpipe实战

LLVM项目深度解析:从三段式架构到llvmpipe实战 很多人以为 llvm-project 就是那个写编译器的开源项目但真正动手拉过仓库、编译过源码、写过 Pass 的人会告诉你这东西的边界远比编译器三个字宽得多。我最早接触 LLVM 是 2016 年在搞 GPU 驱动里的 JIT 编译那时候 llvm-project 的仓库还没像现在这样把 Clang、lld、libc 全部收编到一起装个环境要分别拉好几个 repo折腾一整天是常事。后来项目重组、monorepo 化再到 LLVM 15.0.7 这个版本成为不少发行版和闭源 SDK 的默认工具链底座整个生态的复杂度又上了一个台阶。这篇文章我就围绕 llvm-project 这个仓库本身来聊结合我实际编译、调试、写 Pass 的经验把这个项目到底由什么组成三段式架构的设计逻辑是什么从源码构建 LLVM 15.0.7 要避开哪些坑llvmpipe 和 256 bits 这些热词背后对应的到底是什么一次性讲清楚。不管你是准备入门编译器开发还是工作中被 CMake 和 Ninja 折磨过又或者只是好奇 Mesa 里的软件渲染器为什么叫 llvmpipe这篇文章都适合你。1. LLVM 到底是个什么项目不是编译器那么简单1.1 从一个学术项目到编译器基础设施的事实标准llvm-project 是一个托管在 GitHub 上的 monorepo单仓库多项目核心是 LLVM 编译器基础设施。它的起源是 2000 年 Chris Lattner 在伊利诺伊大学读博期间开始设计的底层虚拟机Low Level Virtual Machine最初的动机是解决静态编译和动态编译之间缺乏统一中间层的问题。但今天你打开这个仓库看到的早就不止一个虚拟机了。LLVM 这个词现在被用来指代好几层东西新手最容易在这里被绕晕LLVM 作为仓库名即 llvm-project包含 LLVM 核心库、Clang、lld、libc、compiler-rt、MLIR、polly、lldb、flang 等十几个子项目。LLVM 作为核心库仓库里的llvm/目录提供 IR中间表示、优化 Pass、目标后端、汇编器、反汇编器、JIT 引擎等能力。LLVM 作为编译器前端很多人说用 LLVM 编译 C 代码实际上指的是 Clang即 LLVM 的 C/C/Objective-C 前端。这三层关系可以用一条编译流水线串起来Clang 把 C/C 源码解析成 LLVM IRLLVM 核心库对 IR 做优化最后交给目标后端生成机器码。这条流水线就是 LLVM 所谓的三段式架构它的价值在于解耦前端只负责看懂语言中端只负责优化逻辑后端只负责适配硬件。1.2 为什么这个架构能改变编译器行业在 LLVM 之前绝大多数编译器是一条龙式的。GCC 虽然也有中间表示GIMPLE但 GCC 的整体架构设计使得复用它的前端或后端都非常困难。如果你想支持一门新语言就得从头写一个编译器包含词法分析、语法分析、类型检查、优化、代码生成工作量大到不现实。如果你想给现有语言换一个新 CPU 架构比如给 Python 解释器加一个 JIT在 GCC 的框架里几乎是不可想象的。LLVM 把这个问题拆开了。它提供的是一套编译器积木前端框架libclang、Clang、flang、rustc 的前端等负责把新语言接到 LLVM 生态。IR 和 Pass 基础设施让优化逻辑可以跨语言复用——不管你是 C 还是 Rust到了 IR 层面大家都用同一套优化器。后端覆盖 x86、ARM、AArch64、RISC-V、PowerPC、MIPS、AMDGPU、NVPTX 等几乎所有主流架构每新增一种后端所有前端语言都受益。这个模型的威力在苹果推动 Clang 替代 GCC、Rust 选择 LLVM 作为默认后端、CUDA 和 ROCm 用 LLVM 做 GPU 编译器的这几件事上体现得淋漓尽致。到今天llvm-project 已经成了编译器基础设施的事实标准这句话不是夸张而是对整个行业的客观描述。2. 三段式架构前端、IR 与后端的解耦逻辑2.1 中间表示IR的设计哲学LLVM IR 是整个项目的心脏它长什么样用 Clang 把一段 C 代码编译成 IRclang -S -emit-llvm你会看到类似这样的内容define i32 add(i32 %a, i32 %b) { %1 add i32 %a, %b ret i32 %1 }这个文本形式的 IR 看起来很像汇编但它有几个关键设计静态单赋值SSAStatic Single Assignment每个变量只能被赋值一次。%1一旦绑定到add的结果就不能再被重新赋值。这使得数据流分析变得极其简单因为每个值的定义和使用关系在语法层面就是显式的。类型系统i32、float、ptr、[4 x i32]这些类型让 IR 在优化时可以安全地进行类型推导不需要像传统汇编那样猜测内存里的数据是什么。无限虚拟寄存器IR 里的临时值理论上无限多不需要考虑物理寄存器的数量寄存器分配交给后端的寄存器分配器去做。IR 有三种形态内存中的llvm::Module对象C 数据结构、磁盘上的可读文本.ll 文件、磁盘上的紧凑二进制.bc 文件。平时调试优化问题最常用的是文本形式发布产物或缓存中间结果用 bitcode 形式更省空间。2.2 从源码到 IR前端做了哪些事以 Clang 为例它把一个.cpp文件变成 IR 要经过这样几个阶段预处理处理#include、#define、条件编译等。词法分析把字符流变成 token 流。语法分析把 token 流变成抽象语法树AST。语义分析类型检查、名字查找、模板实例化等。生成 IR把 AST 降级lower为 LLVM IR。这里有一个值得注意的点Clang 的 ASTclang::ASTContext里的数据结构和 LLVM IR 是两套完全不同的东西。AST 保留了丰富的源码信息——宏展开位置、类型别名、模板参数——这些对 IDE、静态分析、重构工具非常重要。IR 则丢弃了大部分源码层面的信息只保留对优化和后端有用的语义。所以在 LLVM 生态里Clang 静态分析器clang-tidy工作在 AST 层面而优化器工作在 IR 层面。理解了这一点你就不会奇怪为什么 clang-tidy 能做命名规范检查这种高度源码相关的事情而优化 Pass 只能做与源代码无关的变换。2.3 Pass 管线优化器是如何炼钢的IR 生成之后并不是直接交给后端而是要经过一系列优化 Pass。Pass 是 LLVM 中最重要的概念之一它的本质是一个对 IR 做某种变换或分析的函数。打个比方生成了 IR 相当于有了粗钢Pass 管线就是炼钢、锻造、精加工的一系列工序。常见优化 Pass 包括Pass 名称作用典型收益场景InstCombine把简单指令模式合并为更高效的指令常量折叠、代数恒等式化简GVN全局值编号消除重复计算公共子表达式消除LoopUnroll循环展开减少循环控制开销小循环体内含大计算量的场景Inliner内联小函数消除调用开销短函数被频繁调用的场景SROA把标量化的聚合类型拆分到寄存器结构体变量的优化DeadCodeElimination删除不可达和无效代码清理前面 Pass 留下的死代码现代的 LLVM 使用 New Pass ManagerNPM通过一个 PassPipelineBuilder 把几十个 Pass 按固定顺序编排起来形成一个面向特定优化等级-O0、-O1、-O2、-O3的流水线。你可以用opt -passesdefaultO2查看默认流水线也可以用-print-afterinstcombine这类参数查看某个 Pass 执行后的 IR 变化这是日常调试优化问题最常用的手段。3. 亲手从源码构建 LLVM 15.0.7完整流程与踩坑实录3.1 构建前的准备磁盘、内存、Ninja 一个都不能少很多人拉完 llvm-project 仓库直接执行cmake .. make然后等了一个小时还在编译最后磁盘满了或者内存爆了心态直接崩掉。LLVM 是全套编译器基础设施里出了名的吃资源大户构建前必须做好准备工作。硬件方面我个人的经验线是这样的内存最低 8GB建议 16GB 以上。链接clang可执行文件的那个环节尤其是使用 Gold 或 BFD ld 时内存峰值能到 4-6GB内存不够会走到 swap速度慢到怀疑人生。磁盘完整构建 Clang LLVM lld compiler-rt源码加构建产物至少需要 40-60GB 空闲空间。如果启用断言DebugAsserts 构建体积会翻倍以上。CPU全核并行编译要开。LLVM 的编译并行度很高-j参数建议设为物理核心数的 1.5 倍左右设到物理核心数不一定能跑满因为链接阶段有大量 I/O 等待。工具链方面Ninja 是必须的。LLVM 官方已经明确支持 Ninja 作为主要构建系统它的增量构建速度比 Makefiles 快太多——我在一次改完头文件后重新构建Ninja 只花了 40 多秒而 Make 跑了将近 4 分钟。装好 CMake3.20 以上、Ninja、GCC 或 Clang 后就可以开始配置了。3.2 CMake 配置的思路你要的是工具链还是编译器CMake 配置是整个构建流程里最有讲究的一步。很多人直接抄官方文档的命令却没搞清楚每个开关是干什么的。这里我给出一个我实际用过的、比较合理的 Release 版配置cmake -G Ninja \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_PROJECTSclang;lld;libcxx;libcxxabi;compiler-rt \ -DLLVM_TARGETS_TO_BUILDX86;AArch64;RISCV \ -DLLVM_ENABLE_ASSERTIONSON \ ../llvm几个关键参数逐个说CMAKE_BUILD_TYPERelease编译 LLVM 自身时启用优化。如果你要调试 LLVM 本身应该用RelWithDebInfo或Debug但 Debug 构建慢得离谱日常做实验推荐RelWithDebInfo。LLVM_ENABLE_PROJECTS这是 monorepo 化之后最重要的开关类似 Python 的pip install里选择装哪些包。这里不需要把全部项目都打开——flang 依赖很多 Fortran 相关的库polly 需要额外的 ISL 依赖默认不开能省大量编译时间。LLVM_TARGETS_TO_BUILD只构建你需要的目标后端。默认是构建全部后端光一个 NVPTX 后端就能让编译时间多出二十分钟。如果你只需要本机开发写X86就够做交叉编译再加对应目标。LLVM_ENABLE_ASSERTIONSON启用 LLVM 内部的断言检查。Release 模式默认关闭断言但如果我要写自定义 Pass 或调试优化问题我一定会打开它——很多 IR 合法性错误在断言模式下会直接报出来而不是静默生成错误代码。配置完成后执行ninja -j16首次全量构建 LLVM Clang lld在 16 核机器上大约需要 30-50 分钟。这个时间取决于你的 CPU、磁盘和配置开关的数量慢是正常的不要急。3.3 我在构建中踩过的坑第一次从源码构建 LLVM 15.0.7 时我遇到过几个典型的坑写出来帮你避开坑一CMake 缓存污染。同一个构建目录换配置参数时如果旧的CMakeCache.txt里残留了某些变量新配置会沿用旧值行为非常诡异。解决办法是换用新的空目录构建或者rm -rf CMakeCache.txt CMakeFiles后重新配置。我建议从一开始就养成不同配置用不同构建目录的习惯比如build-release、build-debug分开。坑二链接阶段内存爆掉。链接libLLVM-15.so和clang可执行文件时内存峰值非常高。一个有效缓解方案是把LLVM_LINK_LLVM_DYLIB打开让所有工具动态链接一个libLLVM共享库而不是每个工具都静态链接一遍。代价是opt、llc这些工具的体积会变小但如果你是在做深度开发这个开关可能影响调试建议只在纯粹想快速拿到可用工具链的场景使用。坑三编译器版本太老。LLVM 15.0.7 要求你的宿主编译器支持 C17。我第一次在 Ubuntu 20.04 上用系统默认的 GCC 9 构建时报了一堆模板相关的编译错误。后来把 GCC 升到 11 才顺利通过。如果你用的发行版自带的编译器偏老建议先装一个新版本的 GCC 作为宿主编译器或者直接用更高版本的 Clang 去编译。构建完之后验证一下./bin/clang --version ./bin/llc --version echo int main(){return 0;} | ./bin/clang -x c - -o /tmp/hello /tmp/hello能正常输出版本号且能编译执行 C 程序工具链就算可用了。4. llvmpipe 是什么藏在 LLVM 里的软件渲染引擎4.1 为什么需要软件光栅化llvmpipe 这个名字经常在 Linux 图形栈的日志里出现比如你在虚拟机里跑 OpenGL 应用glxinfo输出的渲染器字符串可能就是llvmpipe (LLVM 15.0.7, 256 bits)。它到底是什么llvmpipe 是 Mesa 项目里一个基于 LLVM 的软件光栅化渲染器software rasterizer。它不依赖任何 GPU完全用 CPU 来执行图形管线的顶点处理和像素着色。它的核心机制是把 GLSL 着色器编译成 LLVM IR再通过 LLVM 的 JIT 引擎生成针对当前 CPU 优化的机器码然后以 SIMD 指令并行处理像素块。这带来的好处是显而易见的在没有 GPU 的服务器、虚拟机、云环境里仍然可以运行 OpenGL/Vulkan 应用。在做图形调试时软件渲染的结果可以作为标准答案用来对比硬件驱动的输出是否正确。对于一些特殊渲染需求比如高精度浮点计算、无 GPU 的 CI 环境跑图形测试llvmpipe 是唯一的可行选择。LLVM 的 JIT 优化能力让 llvmpipe 的性能在软件渲染器里属于第一梯队比传统的逐像素解释执行快一个数量级以上。4.2 256 bits 的含义向量宽度与 SIMD你在glxinfo输出里看到的256 bits指的是 llvmpipe 使用 256 位宽的 SIMD 向量寄存器即 AVX2 的ymm寄存器来并行处理像素数据。一个ymm寄存器可以同时装 8 个 32 位浮点数也就是说llvmpipe 一次指令可以处理 8 个像素的同一分量比如同时给 8 个像素做一次颜色乘法。这个256 bits直接决定了 llvmpipe 的像素处理吞吐量。它来自 LLVM 对宿主机 CPU 特性的自动探测当 JIT 编译像素着色器时LLVM 会检查当前 CPU 是否支持 AVX2如果支持就把 IR 向量化为 256 位宽的 SIMD 运算。如果你的 CPU 只支持 SSE2128 位你会看到128 bits的字样支持 AVX-512 的 CPU 在特定配置下甚至能到512 bits。这个机制的技术核心是 LLVM 的向量化能力。llvmpipe 在 JIT 之前会尽量把着色器的内部表示按像素块的方式向量化。如果一个像素着色器对每个像素做相同的计算那么这些计算天然就是数据并行的正好匹配 SIMD 的模型。这里值得感叹的是LLVM 本身并不知道自己在图形渲染它只是忠诚地执行了「把向量 IR 映射到目标 CPU 的向量指令集」这一本职工作。4.3 llvmpipe 的现代价值在 Vulkan 时代llvmpipe 也推出了对应的软件实现lavapipeVK_ICD_FILENAMES.../lvp_icd.x86_64.json启用。你可以用它在没有 GPU 的机器上跑 Vulkan 计算着色器甚至跑一些机器学习推理实验。虽然性能远不如真实 GPU但它的存在让软件栈先行成为可能硬件驱动还没适配某个新扩展时软件渲染器可以先实现并用于验证规范、测试应用兼容性。从 llvmpipe 身上你能看到一个非常漂亮的递归LLVM 是一个编译器基础设施它被用来构建一个软件 GPU这个软件 GPU 的核心工作是 JIT 编译着色器而 JIT 编译出的机器码又利用了 LLVM 的优化能力。编译器是被编译器的技术堆叠驱动的这种自举式的工程美学是 llvm-project 独有的魅力。5. 子项目全家桶Clang、lld、libc、compiler-rt 怎么选怎么用5.1 Clang不只是能用Clang 是 LLVM 的 C/C/Objective-C 前端也是 llvm-project 里用户面最广的子项目。和 GCC 相比它对普通开发者最直观的差异是语法错误提示极其友好。GCC 报错时经常只给你expected ; before }而 Clang 会高亮出错的行列位置给出祖先 token链和修复建议。这可能听起来是小事但在大型 C 工程里一个能精确指出模板实例化出错位置的编译器能省下大量时间。Clang 还派生出了很多工具clangd基于 Clang 的语言服务器为 VS Code、Neovim、CLion 等编辑器提供精确的补全、跳转、重构功能。它读取compile_commands.json来理解每个文件的编译参数是现在 C 开发体验的天花板。clang-tidy基于 AST 的静态分析工具支持上千条检查规则也支持你写自定义 checker。clang-format代码格式化工具配置一个.clang-format文件放到仓库根目录所有协作者的代码风格就能保持一致。AddressSanitizerASan、UndefinedBehaviorSanitizerUBSan、ThreadSanitizerTSan这些运行时消毒器由 compiler-rt 提供配合 Clang 使用。ASan 在捕获内存越界、use-after-free 方面的能力已经成了 C 测试的标配。5.2 lld链接速度的碾压lld 是 LLVM 的链接器。我印象最深的一次对比是同一个大型 C 项目用 GNU ld 链接需要 90 秒用 lld 只需要 8 秒。差距接近 11 倍。这个速度优势在多线程链接、增量开发场景下非常明显很多大厂在 CI 里把链接器换成 lld 之后整个构建时间下降了 20%-30%。用 lld 替换系统默认链接器的方式很简洁clang -fuse-ldlld main.cpp ...或者使用 LLD 的兼容名称让构建系统按原链接器调用它ln -s /usr/local/bin/ld.lld /usr/local/bin/ldlld 支持 ELF、Mach-O、COFF 和 WebAssembly 等格式这意味着 Linux、macOS、Windows 的链接体验可以统一。不过在 macOS 上Apple 的 ld64 和 lld 的 Mach-O 支持还存在一些差异生产环境仍需谨慎。5.3 其他子项目的定位除了 Clang 和 lldllvm-project 里还有几个值得你知道的项目libc / libcabiLLVM 的 C 标准库实现。如果你在 macOS 或嵌入式平台上开发libc 几乎就是默认选择。它和 libstdc 的 ABI 不兼容混用会出链接问题这一点在切换工具链时要格外注意。compiler-rt提供运行时支持库包括 sanitizer 运行时、__atomic内建函数的实现、libfuzzer、profile等。它不是你直接链接的库而是编译器在生成某些特殊代码时自动引用的底层实现。MLIR多层级中间表示框架。它是从 LLVM 派生出来的用来构建编译器的编译器TensorFlow、PyTorch 的编译器后端都有 MLIR 的身影。如果你对领域特定编译器感兴趣MLIR 是当前最热的方向之一。Polly面向循环嵌套和多面体模型的优化器可以自动做循环分块、向量化、数据局部性优化。虽然生产环境实际默认启用的场景不多但研究价值很高。LLDB基于 LLVM 的调试器。它提供了一个可复用的调试基础设施组件库很多集成开发环境用它作为调试后端。我个人在写 Pass 时不太用 LLDB但它在 macOS 上是 Xcode 的默认调试器。flangFortran 前端。这个项目在 LLVM 15 时已经基本达到了生产可用程度对科学计算领域意义重大。6. 我用 LLVM 做过的事Pass 开发与调试的真实经验6.1 写一个最简单的 Function Pass很多人接触 llvm-project 的第一个动手目标就是写一个自定义 Pass。我用一个具体例子来说明这个流程。假设你要写一个 Pass把 IR 里所有的add指令替换成sub指令纯粹为了演示没实际意义在 LLVM 15 的 New Pass Manager 框架下创建一个目录MyPass/里面放三个文件。CMakeLists.txtadd_llvm_pass_plugin(MyPass MyPass.cpp PLUGIN_TOOL opt )MyPass.cpp#include llvm/IR/Function.h #include llvm/IR/InstIterator.h #include llvm/IR/Instructions.h #include llvm/Pass.h #include llvm/Passes/PassBuilder.h #include llvm/Passes/PassPlugin.h using namespace llvm; namespace { struct MyPass : public PassInfoMixinMyPass { PreservedAnalyses run(Function F, FunctionAnalysisManager AM) { bool Changed false; for (Instruction I : instructions(F)) { if (auto *BinOp dyn_castBinaryOperator(I)) { if (BinOp-getOpcode() Instruction::Add) { IRBuilder Builder(BinOp); Value *LHS BinOp-getOperand(0); Value *RHS BinOp-getOperand(1); Value *Sub Builder.CreateSub(LHS, RHS); BinOp-replaceAllUsesWith(Sub); BinOp-eraseFromParent(); Changed true; } } } return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); } }; } // namespace PassPluginLibraryInfo getMyPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, MyPass, LLVM_VERSION_STRING, [](PassBuilder PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager FPM, ArrayRefPassBuilder::PipelineElement) { if (Name my-pass) { FPM.addPass(MyPass()); return true; } return false; }); }}; } extern C LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return getMyPassPluginInfo(); }编译和运行mkdir build cd build cmake -G Ninja -DLLVM_DIR/path/to/llvm-project/build/lib/cmake/llvm .. ninja opt -load-pass-pluginlibMyPass.so -passesmy-pass -S input.ll这个例子里有几个关键点add_llvm_pass_plugin是 LLVM 提供的 CMake 宏它会自动生成一个可被opt动态加载的插件库。这就是 LLVM 15 时代官方推荐的 Pass 开发方式不需要重新编译整个 LLVM只需要用系统里的 LLVM 开发包编译插件。PassInfoMixinMyPass是 New Pass Manager 的 mixin 接口要求你实现run方法。PreservedAnalyses告诉优化管线你这个 Pass 保留了哪些分析结果。如果你对 IR 做了修改必须返回PreservedAnalyses::none()否则后续 Pass 可能使用过期的分析结果导致错误。注册 Pass 到管线的回调里Name my-pass对应命令行里-passesmy-pass的字符串名字对不上会直接报unknown pass name。6.2 LLVM 调试的三大工具写 Pass 的过程中有三大工具是我每天必用的没有它们我几乎无法定位问题工具一opt的打印选项。opt -print-after-all -passes...会打印每个 Pass 执行后的 IR。配合-filter-print-funcs函数名可以只看某个函数的 IR 变化。这个能力用于回答哪个 Pass 把 IR 变成了这个样子。工具二llvm-as和llvm-dis。它们把文本 IR 转成 bitcode、bitcode 转回文本。手上有.bc文件时先用llvm-dis看一下里面的 IR比直接读二进制高效太多。有些时候一个诡异的 bug 是因为 bitcode 是由不同版本的 LLVM 生成的llvm-dis的报错信息能帮助你快速识别版本不匹配。工具三-debug-only日志。LLVM 内部有大量LLVM_DEBUG(dbgs() ...)输出。传-debug-onlyloop-unroll之类的作用域参数可以只看某个功能的调试日志。这个机制比打印整个 IR 更适合定位算法层面的逻辑错误。还有一个软件工程层面的工具git bisect。如果你发现某个 IR 变换在某次更新后行为变了而且你有 LLVM 的历史提交记录git bisect可以在 O(log n) 次构建内帮你找到引入问题的那个 commit。这个方法在追踪上游回归问题时极其有效我至少用它找到过三次我自己的 Pass 在移植到新版本后失灵的根因。6.3 给新手的建议最后聊一些关于学习路径的体会。我见过太多人一上来就啃 LLVM 的源码从llvm/lib/IR开始一行行读结果两周后放弃了——因为 LLVM 代码库的量级不是一个适合从头到尾阅读的东西。它更像一座巨大的城市你要做的是先找一张地图确定自己的目的地然后只走需要的路线。我的建议是按这个顺序入门先用现成的 Clang 把 C 代码编译成 IR熟悉 IR 的文本格式。能看懂%i、alloca、load、store、br这些基础指令就算过关。用opt跑现成的 Pass配合-print-after-all观察 IR 变化建立优化器做了什么的直觉。写一个最简单的自定义 Pass做一些机械的 IR 变换跑通插件加载和调试循环。遇到具体需求比如循环优化、内联决策、寄存器分配问题再深入对应的源码目录用LLVM_DEBUG追踪执行路径。多读上游的 review 和 commit message。LLVM 社区有严格的 code review 文化每个 commit 都有清晰的动机说明这比任何文档都更能教你为什么要这么设计。整个 llvm-project 的学习曲线确实陡峭它不是一个两星期能看完的项目。但它的回报也极其丰厚你学会的 IR 思维、Pass 开发模式、编译器架构解耦思想可以迁移到任何涉及源代码到目标代码的系统里——脚本语言 JIT、数据库查询优化器、GPU 着色器编译、甚至正则表达式引擎。这也是为什么这么多年过去了我依然认为 llvm-project 是每个系统程序员都应该至少深入一次的基础设施项目。
返回列表