十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Cranelift 对比 LLVM:Wasmtime 代码生成器的单层 IR 架构与设计取舍

Cranelift 对比 LLVM:Wasmtime 代码生成器的单层 IR 架构与设计取舍 Cranelift 对比 LLVMWasmtime 代码生成器的单层 IR 架构与设计取舍【免费下载链接】wasmtimeA lightweight WebAssembly runtime that is fast, secure, and standards-compliant项目地址: https://gitcode.com/gh_mirrors/wa/wasmtimeCranelift 是 WebAssembly 运行时 Wasmtime 默认使用的代码生成器而 LLVM 是业界最流行的编译基础设施之一。本文以cranelift/docs/compare-llvm.md为核心骨架系统对比两者在中间表示IR分层、程序结构、SSA 形式、值类型与指令集设计上的异同并结合当前仓库的cranelift-codegen、cranelift-module等 crate 源码解释 Cranelift 为何选择单层 IR 贯穿整个代码生成流程这一激进但务实的设计以及它如何支撑 Wasmtime 追求快速编译与快速实例化的目标。读完本文你将理解 Cranelift 与 LLVM 架构差异的本质并能从源码层面定位两类编译器的关键设计证据。一、两个表面相似的编译器项目LLVM 是一套以 C 库形式实现的编译器组件集合既可以用来构建 JIT 编译器也可以构建像 Clang 这样的静态编译器因此备受推崇。Cranelift 与 LLVM 在表面上非常相似很容易被误认为同类项目。事实上两者确实共享一批重要的设计前提使用大致与 ISA 无关的输入语言从而将目标指令集架构之间的差异尽量抽象掉深度依赖 SSA 形式静态单赋值主要中间表示同时具有文本形式与内存形式LLVM 额外还有二进制的 bitcode 格式Cranelift 没有可面向多个 ISA 生成代码默认即可交叉编译无需重新构建代码生成器。然而在这些共同点之下两者的中间表示架构存在根本性差异而这些差异决定了各自的适用场景LLVM 追求一套基础设施服务所有前端与后端Cranelift 则追求在 Wasmtime 这种运行时场景下以最快速度把 IR 变成可执行机器码。二、中间表示架构LLVM 的多层流水线 vs Cranelift 的单层 IR2.1 LLVM 的四层 IRLLVM 在把一个程序翻译成二进制机器码的过程中会依次使用多种中间表示LLVM IR最主要的中间表示同时具有文本、二进制bitcode和内存三种形式。它承担两个核心职责作为与 ISA 无关、相对稳定的输入语言方便前端生成作为中层优化mid-level optimizations的载体大量分析与变换 pass 运行在这一层。SelectionDAG一种基于图的、以单个基本块为单位的表示供指令选择器使用同时包含 ISA 无关与 ISA 相关的 opcode。针对该表示主要运行以下 pass类型合法化type legalization消除所有在目标 ISA 寄存器中没有对应表示的值类型操作合法化operation legalization消除所有无法映射到目标 ISA 指令的 opcodeDAG 合并DAG-combine在合法化之后清理冗余代码指令选择instruction selection把 ISA 无关的表达式翻译成 ISA 相关的指令。SelectionDAG 表示还会自动消除公共子表达式与死代码。MachineInstrMI一种线性的 ISA 相关指令表示初始处于 SSA 形式但在寄存器分配期间及之后也可以表示非 SSA 形式。大量底层优化运行在 MI 上其中最重要的两类 pass 是指令调度scheduling与寄存器分配register allocation。MCLLVM 的输出抽象层也是 LLVM 内置汇编器的基础用于分支松弛branch relaxation、输出汇编或二进制目标代码、汇编器与反汇编器。此外LLVM 有一个进行中的global instruction selection全局指令选择项目试图用 MachineInstr 表示上的 ISA 无关 opcode 取代 SelectionDAG部分目标 ISA 也拥有快速指令选择器可以在可能时直接把简单代码翻译成 MachineInstr绕过 SelectionDAG。2.2 Cranelift单一 IR 覆盖所有抽象层次与 LLVM 的多层 IR 不同Cranelift 使用一种中间表示覆盖上述所有抽象层次。这之所以可行很大程度上是因为 Cranelift 的范围更小Cranelift不提供汇编器与反汇编器因此无需表示 ISA 中每一种奇怪的指令只有代码生成器实际会发射的指令才需要表示Cranelift 的 opcode 是ISA 无关的但在合法化 / 指令选择之后每条指令都会被标注一个ISA 相关的编码encoding代表一条具体的原生指令SSA 形式全程保留。寄存器分配之后每个 SSA 值都被标注上分配的 ISA 寄存器或栈槽。从仓库源码可以印证这一编码标注机制。在 cranelift/codegen/src/isa/mod.rs 中可以看到Legalize类型当一条指令没有编码时Encodings迭代器会返回一个合法化函数来转换该指令这正是合法化 / 指令选择阶段给指令附上 ISA 编码这一设计的代码级体现。Cranelift 的中间表示与 LLVM IR 相似但抽象层次略低以便贯穿整个代码生成过程。这种设计取舍的代价是Cranelift IR 对中层优化不够友好。Cranelift 目前并不执行中层优化如果未来这变得重要其愿景是增加一个独立的 IR 层或独立的 IR来支撑。届时流程将不是前端生成优化 IR再翻译为代码生成 IR而是前端直接生成代码生成 IR需要时可翻译成优化 IR 再翻译回来。这个方向使整个系统在不需要中层优化时偏向快速编译——例如发射未优化代码或底层优化已足够时。同时它也解除了中层优化 IR 设计空间中的一些约束使诸如基于VSDGValue State Dependence Graph的 IR这类想法变得更为可行。2.3 从源码看 Cranelift 的单层 IR 结构Cranelift IR 的两种形式内存数据结构与文本格式在 cranelift/docs/ir.md 中有完整参考。IR 文本文件使用.clif扩展名是测试用例与调试输出的载体。下面这个经典的average函数示例来自 ir.md编译自一个求浮点数组平均值的 C 函数展示了单层 IR 的完整形态test verifier function %average(i64, i64) - f32 system_v { ss0 explicit_slot 8 ; Stack slot for sum. block1(v0: i64, v1: i64): v2 f64const 0x0.0 stack_store v2, ss0 brif v1, block2, block5 ; Handle count 0. block2: v3 iconst.i64 0 jump block3(v3) block3(v4: i64): v5 imul_imm v4, 4 v6 iadd v0, v5 v7 load.f32 v6 ; array[i] v8 fpromote.f64 v7 v9 stack_load.f64 ss0 v10 fadd v8, v9 stack_store v10, ss0 v11 iadd_imm v4, 1 v12 icmp ult v11, v1 brif v12, block3(v11), block4 ; Loop backedge. block4: v13 stack_load.f64 ss0 v14 fcvt_from_uint.f64 v1 v15 fdiv v13, v14 v16 fdemote.f32 v15 return v16 block5: v100 f32const NaN return v100 }注意这里循环、浮点提升/降级、栈槽读写、条件分支全部出现在同一层 IR 中——这正是单层 IR 覆盖从语义到发射前所有层次的直接体现。在内存形式中函数体function body由若干以终止指令terminator结尾的基本块组成执行永远不会穿透到下一个块。三、程序结构LLVM 的 Module 与 Cranelift 的自包含函数在 LLVM IR 中最大的可表示单元是module大致对应一个 C 翻译单元它是函数与全局变量的集合也可能包含对外部符号的引用。而在 Cranelift 的 IR由cranelift-codegencrate 使用中函数是自包含的可以独立编译这一层没有显式的 module 容器。模块功能在 Cranelift 中由可选的库层提供即cranelift-modulecrate。它提供处理模块的工具模块可以包含多个函数以及数据对象并能将它们链接在一起。在 cranelift/module/src/lib.rs 中可以看到该 crate 对外导出的核心类型DataDescription、DataDeclaration、DataId、FuncId、FunctionDeclaration、Linkage、Module、ModuleReloc、ModuleRelocTarget等。Moduletrait定义于 cranelift/module/src/module.rs提供了declare_function、declare_data、define_function、define_data等接口以及把模块级符号引入单个函数/数据对象上下文的方法如declare_func_in_func、declare_data_in_func。这种分层明确体现了设计哲学核心代码生成器只关心单函数模块与链接语义被隔离在可选库中。SSA 表示的形态差异phi 指令 vs 基本块参数LLVM 与 Cranelift 都用基本块basic block图作为函数的 IR但处理 SSA 数据流的方式不同LLVM 在其 SSA 表示中使用phi 指令Cranelift 则向基本块传递参数BB 参数。两种表示在表达能力上是等价的但 BB 参数更擅长处理多个分支以不同参数跳到同一目标块的情形。向 BB 传参看起来非常像函数调用传参寄存器分配器对它们的处理也极其相似参数会被分配到寄存器或栈位置。在 cranelift/docs/ir.md 的 SSA 一节中详细说明了这一点每个值恰好被定义一次每次使用都必须被定义点支配Cranelift 没有 phi 指令而是使用带类型参数列表的 BB——每当控制流转入某个 BB就必须提供对应的实参函数入口处函数参数作为实参传给入口块的参数。上面average例子中循环归纳变量i由三个 SSA 值表示block2中的初始值v3、循环块block3的 BB 参数v4每次迭代的值、以及下一轮迭代的值v11。对于需要变量可多次赋值再转成 SSA的场景cranelift_frontendcrate 提供了相应工具或者可以把变量表示为栈槽stack slot用stack_store/stack_load/stack_addr访问。四、值类型对比贴近寄存器 vs 高抽象Cranelift 的类型系统 大部分是 LLVM 类型系统的子集抽象程度更低更贴近常见 ISA 寄存器能容纳的类型维度LLVMCranelift整数类型任意位宽仅 2 的幂次i8到i64浮点类型多种精度仅f32与f64即 WebAssembly 提供的两种未来可能加入 16 位与 128 位地址表示包含 pointee 类型的丰富指针类型地址用整数表示没有指针类型未来可能增加单一的 address 类型SIMD 向量任意数量的 lanelane 数必须为 2 的幂最多 256聚合类型命名/匿名 struct 与 array没有聚合类型布尔类型i1用值为0或1的整数表示多返回值用单个聚合类型值建模指令与函数调用可直接返回多个结果值关于布尔值还有一个值得注意的细节有尺寸的 Cranelift 整数类型被用来表示 SIMD 向量掩码例如i32x4其中每个 lane 要么全 0、要么全 1。在源码层面cranelift/codegen/src/ir/types.rs 中的as_truthy方法显示比较运算的标量结果类型是i8而不是 LLVM 的i1向量比较则返回同 lane 数的整数向量——这正是用整数表示布尔的实现证据。从 types.rs 还可以看到Type::int()只接受 8/16/32/64/128 位与文档描述的整数类型限于 2 的幂次、从 i8 到 i64完全一致i128也是近年加入的扩展类型。这些类型的语义在 ir.md 中有完整定义整数类型本身无符号语义由具体指令按有符号或无符号解释浮点类型遵循 IEEE 754 语义但不支持非默认舍入模式、非屏蔽异常与异常标志NaN 编码遵循 IEEE 754-2008 建议静默 NaN 的尾数最高位置 1信号 NaN 置 0。五、指令集设计小而精的 LLVM 指令集 vs 大而全的 Cranelift 指令集LLVM 有一个小而定义良好的基础指令集外加大量 intrinsic其中一些是 ISA 相关的。Cranelift 则拥有更大的指令集且没有 intrinsics某些 Cranelift 指令本身就是 ISA 相关的。由于 Cranelift 指令要一直用到二进制机器码发射为止所以每一个可生成的原生指令都需要有对应的 opcode。不同 ISA 之间重叠很多例如iadd_imm寄存器加立即数就被所有能执行寄存器加立即整数的 ISA 使用。一个简单的 RISC ISA如 RISC-V只用共享指令就能定义而 x86 需要一批特定指令来建模寻址模式。配合 cranelift/docs/ir.md 中指令组instruction groups一节可以看得更清楚所有共享指令都属于base指令组目标 ISA 可以在自己的指令组中进一步定义指令。这种共享为主、ISA 补充的结构正是交叉编译默认可行、且每个后端实现成本可控的原因。六、Cranelift 单层 IR 的实际落地一次编译旅程把上面的设计落到仓库源码中可以看到单层 IR 从构建到发射的完整路径构建 IR前端如 Wasmtime 的翻译层或cranelift_frontend生成ir::Function。函数体内的指令、值、基本块以 entity 索引形式组织——cranelift/codegen/src/ir/entities.rs 中定义了Value、Block、StackSlot、GlobalValue、JumpTable、FuncRef、SigRef等实体全部以u32索引编码。面向目标配置TargetIsatrait 与TargetFrontendConfig见 cranelift/codegen/src/isa/mod.rs向前端提供目标相关信息例如默认调用约定default_call_conv、指针宽度pointer_width决定地址是i32还是i64与页大小对齐。合法化与编码当指令在当前 ISA 上没有直接编码时Encodings迭代器返回Legalize函数isa/mod.rs将指令改写为可编码的形态再为每条指令附上 ISA 编码。寄存器分配与发射Context::compile等入口cranelift/codegen/src/context.rs使用regalloc2完成寄存器分配SSA 值被标注寄存器或栈槽后最终由各 ISA 后端的emit模块发射机器码。这整个过程都在同一种 IR上流转不存在 LLVM 那种 LLVM IR → SelectionDAG → MachineInstr → MC 的多层翻译。对 Wasmtime 而言这意味着每个 WebAssembly 函数从字节码到机器码的路径更短、中间结构更少从而更贴合 Wasmtime 面向快速编译、快速实例化场景参见 benches/compile、benches/instantiation 等基准目录的定位。七、总结两种架构哲学的选择对比维度LLVMCraneliftIR 层次多层LLVM IR / SelectionDAG / MachineInstr / MC单层Cranelift IR模块模型module 为核心单元函数自包含模块由可选cranelift-module层提供SSA 数据流phi 指令基本块参数BB 参数值类型抽象高任意位宽整数、指针、聚合类型低贴近寄存器无指针与聚合类型指令集小指令集 大量 intrinsics大指令集、无 intrinsics、含 ISA 相关 opcode汇编器/反汇编器MC 层内置不提供中层优化强大的 pass 库当前不执行未来考虑独立优化 IR 层适用场景通用编译器基础设施JIT 与静态编译面向 Wasmtime 等运行时的高速代码生成LLVM 用多层 IR 强大 pass 库换取了对任意前端、任意目标、任意优化级别的通用性与可扩展性Cranelift 则用单层 IR 更贴近机器的类型与指令集换取了更短的编译路径与更低的后端复杂度。两者共享 SSA、ISA 无关输入语言、多 ISA 支持与默认可交叉编译等设计基因却在中间表示这条主线上走向了截然不同的分岔。对希望深入理解编译器中 IR 分层设计的读者建议按以下路径继续研读当前仓库完整 IR 参考cranelift/docs/ir.md值类型、立即数、控制流、调用约定、内存访问、实现上限等类型系统实现cranelift/codegen/src/ir/types.rsIR 实体与指令定义cranelift/codegen/src/ir/entities.rs、cranelift/codegen/src/ir/instructions.rs模块层多函数、数据对象与链接cranelift/module/src/module.rs、cranelift/module/src/lib.rs目标 ISA 抽象与合法化cranelift/codegen/src/isa/mod.rs编译入口与寄存器分配cranelift/codegen/src/context.rs【免费下载链接】wasmtimeA lightweight WebAssembly runtime that is fast, secure, and standards-compliant项目地址: https://gitcode.com/gh_mirrors/wa/wasmtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表