为什么 TypeScript 选择 LLVM?
提前编译器所处的环境和 JIT 完全不同。JIT 在用户等待的过程中进行 编译,因此编译延迟才是约束条件。而像 Perry 这样的 AOT 编译器只 编译一次——在开发者的机器上或 CI 中——之后这个二进制文件会被执 行成千上万次。正是这种不对称,让一个重量级优化器物有所值。
LLVM 带来了二十年中间端工作的积累:循环向量化、循环不变量外提、 全局值编号、稀疏条件常量传播、激进内联、别名分析。Perry 的任务 是把它真正能够优化的 IR 交给这套机制——而这正是 TypeScript 类 型信息发挥作用的地方。
降级流水线
源代码先由 SWC 解析,然后降级为一种带类型的高级 IR(HIR),在 LLVM 看到代码之前,真正有意思的决策都在这一步发生:
- 单态化。 泛型函数和类会按每一次具体实例化进行特化,这与 Rust 和 C++ 采用的策略相同。
Stack<number>和Stack<string>会 变成两个独立的、完全带类型的函数——因此优化器面对的是具体类 型,而不是一团泛型分派逻辑,泛型在运行时不产生任何开销。 - 静态分派。 在编译期就已知接收者类型的地方,方法调用会被编译为 LLVM 可 以内联的直接调用,而不是哈希表查找。
- 直接字段访问。 对象字段会被解析为编译期索引,因此属性读取是一次固定偏移的 加载——而不是字典查找。
NaN 装箱与内联降级
对于动态值,Perry 使用 NaN 装箱:每个值都是一个 64 位字。双精 度浮点数被直接存储;对象、字符串、布尔值、null 和 undefined 则被编码进 IEEE 754 安静 NaN 未使用的位模式中。数字是零成本的——不需要装 箱,算术运算也不需要分配内存。
问题在于,对非数字值的操作需要一套“拆包—操作—重新打 包”的位运算序列。如果这些序列以调用单独编译的运行时的形 式存在,LLVM 看到的就是不透明的黑盒,无法跨越它们进行优化。因 此 Perry 把热点操作——属性读取、方法分派、对象分配——直接以内 联 LLVM IR 的形式生成,让优化器可以对其进行融合和化简。举例来 说,对象分配会被编译为一次内联的线程本地 bump 分配:
%off_ptr = getelementptr i8, ptr %state, i64 8
%offset = load i64, ptr %off_ptr ; current bump offset
%new_off = add i64 %offset, 96 ; headers + 8 fields
%sz_ptr = getelementptr i8, ptr %state, i64 16
%size = load i64, ptr %sz_ptr ; block capacity
%fits = icmp ule i64 %new_off, %size
br i1 %fits, label %fast, label %slow为什么不用 Cranelift?
Perry 最初的后端是 Cranelift——wasmtime 背后的代码生成器,为 快速、可预测的编译而设计。它是一个正确的起点,对于 JIT 和沙箱 化运行时来说,它至今仍是一个出色的选择。有两件事迫使 Perry 换 掉了它:
- 优化器天花板。 Cranelift 刻意被设计成一个快速的单层编译器:“decent code quickly(快速产出还不错的代码)”,这对 JIT 来说 是正确的取舍,但对一个卖点是原生峰值性能的 AOT 编译器来说 却是错误的。
- arm64_32。 Apple Watch 使用一种 Cranelift 不支持的 ABI(64 位指令,32 位指 针)。要让 watchOS 成为一个目标平台,就必须使用 LLVM——而同 时维护两个后端意味着两套 bug、两套测试和两套性能基线。
这次迁移并非没有代价:第一个纯 LLVM 版本在部分基准测试上出现 了高达 70 倍的性能回归,因为热点操作最初都要经过不透明的运行 时辅助函数调用。经过修复——内联降级、上文提到的 bump 分配器、 更好的内联边界划分——后端的表现超过了 Cranelift 的数字,等到 尘埃落定时,Perry 在其基准测试套件的每一项上都击败了 Node.js,幅度从 1.7 倍到 24.6 倍,另有两项打平(2026 年 4 月)。完整的复盘值得一读: 从 Cranelift 到 LLVM。
深入了解
编译器内部原理页面 更详细地介绍了 NaN 装箱、单态化和静态分派。在博客上, 优化一切 逐个版本梳理了这些优化工作,而 分代 GC、惰性 JSON,以及经得起推敲的基准 则解释了基准测试方法论是如何运作的(RUNS=11,中位数 + p95)。想了解全貌,可以从 TypeScript 原生编译器 概览开始。