Relax 发布 Rust JIT 编译器 构建提速至 11 秒
Relax 是 Makepad 团队用 Rust 编写的实时编译器与 JIT 运行时,绕过 rustc、LLVM 和链接器,直接从源码运行 Cargo 包,按函数首次调用时编译并热更新。官方数据显示大型 3D 游戏冷启动首帧从约 3 分钟降至 11 秒,二次运行仅 1.15 秒,且支持在运行中打补丁保留状态。
报道正文
本文目录12 个章节
这是该页面的完整纯 HTML 文本,供没有 JavaScript 的读者阅读。相同文本也出现在 README 和纯 HTML 页面中。
Relax
无需等待的 Rust。
rustc 先构建你的程序,然后你再运行它。Relax 则在构建程序的同时运行它。
Relax 是一个用 Rust 编写的 Rust 实时编译器和 JIT 运行时。它直接从源码运行 Cargo 包,整个流程中不需要 rustc、LLVM 或链接器:它在每个函数首次被调用时编译该函数,在程序运行期间优化热点函数,并将你的编辑补丁应用到正在运行的进程中。
Relax 与 cargo 和 rustc 的对比
| cargo + rustc | Relax | |
|---|---|---|
| 大型 3D 游戏,干净检出 → 首帧 | 构建约 3 分钟,然后启动 | 在 M4 MacBook 上11 秒,直接从源码运行 |
| 同一游戏,第二次运行 → 首帧 | 启动已构建的二进制文件 | 在 M4 MacBook 上1.15 秒,无需重新编译 |
| 小型应用 → 首帧 | 先构建 | 冷启动 3.7 秒,热启动 0.41 秒(M4 MacBook) |
| 在运行中的程序里修改一个函数 | 重新构建、重新链接、重启:状态丢失 | 热补丁:状态保留 |
| 保存文件 → 重新运行 92 个测试 | 先重新构建测试二进制文件 | 保存后 2.2 秒 |
| 类型检查与借用检查 | cargo check;构建会先检查所有内容 | relax check;运行时会在每个函数执行前对其进行类型检查,并在后台进行借用检查 |
| 优化代码的速度 | 1.00×(--release) | 更慢:x64 上耗时 1.12×,arm64 上 1.24×(21 个工作负载的几何平均值) |
| 依赖需求 | rustc、LLVM、链接器 | 一个 Rust 库:438k 行,0 个 crates.io 依赖 |
针对 x86-64 和 AArch64 的代码生成;从全新克隆的仓库出发,它能在 Linux x86-64 和 macOS arm64 上完成构建并通过测试。Windows 未经验证。
权衡取舍
- 优化后的代码更慢。 总体而言,Relax 运行的代码比 rustc 的
--release更慢:在 x64 上耗时是 1.12 倍,在 arm64 上是 1.24 倍(21 个工作负载的几何平均值),对比原生 CPU 构建则分别是 1.22 倍和 1.25 倍。它只在少数工作负载上更快:参见 LLVM 仍然胜出的地方。 - 内存占用更多。 编译器、它的缓存以及它发布的每个代码版本都与你程序的进程共享内存,而且 JIT 代码是只追加的。
relax inspect <pid> memory显示内存分布,code显示增长情况。包的target/makepad-run中的代码缓存对于hello会占用 120 到 190 MB。 relax check目前还不比cargo check快。 在大型包上,冷检查比cargo check耗时更长、占用内存更多,而且一次编辑可能触发超出必要的重新检查。我们正在改进这一点。- 并非每个 crate 都能运行。 libs/relax/KNOWN_GAPS.md 列出了尚未解决的缺口,每个都附有复现步骤。
- 并非每次编辑都能实时打补丁。 函数体可以。结构体布局、导入、trait 或 impl 头部、或宏的变更需要重启,Relax 提供了这一功能:
relax inspect <pid> session relaunch。 - 借用检查器与程序并行运行。 运行时会在每个函数执行前对其进行类型检查,并在后台进行借用检查;
relax run --check-first则会等待借用检查完成。 - 不是发布构建。 Relax 用于编辑-运行循环。发布时请使用
cargo build --release构建的产物。
JIT 能带来什么
relax run 会打印一行 relax-remote: pid <pid>。下面除 run 和 test 之外的每条命令都是 relax inspect <pid> <command>,即运行中的程序在运行时回答的一个问题。
| 功能说明 | 命令 | |
|---|---|---|
| 热补丁,保留状态 | 保存文件:发生变更的函数会被重新检查、重新编译并替换到正在运行的程序中。若调用到无法编译的代码,则会等待编辑修复后再继续。 | relax run <package> |
| 保存时运行测试 | 保持测试处于热状态,并重新运行那些进入你所修改函数的测试。 | relax test --live <package> |
| panic 时暂停 | panic 会将其线程连同完整栈一起挂起:在终端中最多 10 分钟,无终端连接时为 2 秒。可读取其局部变量、在其栈帧中求值 Rust 表达式,然后让其展开,或从入口重新运行 restartable 函数。 | panics、locals panic=P1、eval <expr>、resume P1 panic |
| 断点,无需调试器 | 命中时会将线程连同完整栈一起停在调用处。 | break <file>:<line>、locals break=K1、continue |
| 挂起原因解析 | 死锁、阻塞、繁忙或空闲:给出判定结论、等待链以及链中线程的栈。 | why-hung |
| 性能分析 | 对 JIT 和原生栈帧进行采样:每线程 CPU、最热函数及其最热行、带各帧层级的调用树、用于火焰图的折叠栈。 | prof s=3、prof view=tree、why-slow |
| 飞行记录器 | 始终开启,内存占用有界:记录最近几分钟的线程、等待、编译和事件,并在 panic、故障或超过 1 秒的卡顿时自动保存。 | rec save |
| 内存与泄漏 | 内存分布(按类型统计的 RSS、使用中的 malloc 与保留的空闲内存、Relax 自身占用),以及按分配栈统计的程序堆,并给出快照之间的泄漏候选。堆跟踪仅在 Linux x86-64 和 macOS arm64 上运行。 | memory、heap start、heap snap a、heap diff a now |
relax inspect <pid> help 列出了其余命令,其中包括 asm <fn> vs=rustc(将 JIT 生成的机器码与 rustc 的机器码并排显示)、count <fn>(精确调用次数)和 counters(硬件计数器)。
试一试
从全新克隆开始,构建 relax 并通过 JIT 运行一个新包:
git clone https://github.com/makepad/relax
cargo build --release --manifest-path relax/Cargo.toml -p relax-runner --bin relax
cargo new hello
cd hello
../relax/target/release/relax run --quiet hello # run main under the JIT
../relax/target/release/relax test hello # run its #[test] fns
../relax/target/release/relax check hello # type- and borrow-check only在包的文件夹中运行 relax,并传入包名而非路径。它会从构建时所在的检出目录读取 Relax 的标准库,因此请将 relax 文件夹保留在原位。
从全新克隆实测:在 16 核 x86-64 Linux 机器上构建 relax 耗时 53 秒,在 10 核 arm64 Mac 上(cargo 限制为 4 个任务)耗时 78 秒。hello 的首次 relax run 分别耗时 0.24 秒和 0.97 秒,后续运行耗时 0.01 至 0.07 秒。Relax 会将编译结果保存在包的 target/makepad-run 中(relax clean 会将其删除),因此每个新包都从冷启动开始。
网络。 像 hello 这样没有依赖的包不需要网络。对于 crates.io 依赖,如果 ~/.cargo 的注册表中已有该包,Relax 会直接取用;否则会自行从 crates.io 下载,并对照索引中的 SHA-256 进行校验。设置 MAKEPAD_RUN_OFFLINE=1 或 cargo 的 CARGO_NET_OFFLINE 后,它绝不会下载。relax fetch <git-url>(tools/run/runner/src/fetch.rs)会将 git 仓库作为单个文件存储在 ~/.makepad-run 下。
代价:一套全新的 Rust 工具链
Relax 不是 rustc 的封装,也不是 LLVM 的插件。它用一个 Rust 库取代了这两者,以及介于其间的 Cargo 和链接器的相关部分。
| 任务 | 在通常的工具链中 | 在 Relax 中 |
|---|---|---|
词法分析、语法解析并展开宏、macro_rules! 和过程宏 | rustc | relax-lex、relax-front |
| 名称解析、trait 求解、类型推断、借用检查、常量求值 | rustc | relax-types |
| 优化:内联、常量折叠、循环、范围 | rustc 的 MIR pass + LLVM | relax-codegen(O1 和 O3) |
| 寄存器分配、生成 x86-64 和 AArch64 机器码 | LLVM | relax-backend |
| 链接与加载 | 系统链接器和加载器 | relax-compiler,直接写入可执行内存 |
| 读取清单并解析 crate 依赖图 | Cargo | relax-base |
构建 core、alloc 和 std | 随附预编译版本 | 由 Relax 从源码编译(134k 行) |
| cargo + rustc | Relax | |
| --- | --- | --- |
| 编译 | 整个 crate 一次性提前编译。 | 每次只编译一个函数,在它首次被调用时进行。 热点函数会在后台逐级提升:解释器 → O1 → O3。#[relax(O2)] / #[relax(O3)] 可提前请求某个优化级别;在 rustc 下该属性不起作用。 |
| 缓存 | 增量产物可能会过期失效。 | 每个结果都以其输入的哈希为键:命中缓存的结果与全新编译所产生的结果一致,因此无需 cargo clean。 |
| 过程宏 | 编译为原生 dylib,然后加载。 | 当与已链接的构建匹配时以原生方式运行,否则在 JIT 上运行。 |
| 原生代码 | 一切都是原生代码。 | 必须保持原生的 crate(操作系统层、GPU、音频)通过 #[relax_api] 导出接口,JIT 代码通过 C ABI 调用这些接口。 |
Makepad 如何使用 Relax
Relax 源自 Makepad 项目。makepad run 及其工具链位于 Makepad 的代码仓库中,而非本仓库;本节介绍 Makepad 如何使用 Relax。
makepad 是 Relax 的运行器,外加一个 Makepad 宿主。使用 makepad run <package> 启动的 UI 应用会获得:
| 应用获得的能力 | 命令 | |
|---|---|---|
| 无需原生构建 | 其窗口会立即打开并显示构建进度;应用的首个窗口随后接管。 | makepad run <package> |
| 保存即补丁 | 编辑会以补丁形式注入正在运行的应用;其状态和窗口保持不变。 | 默认开启 |
| 原地重启 | 无法打补丁的编辑(类型的布局、导入、trait 或 impl 头部、宏)会在标题栏的 JIT 环上显示“Update ready”。点击后会在同一进程和窗口中重启应用,保留其状态(AppMain::restart_state)和窗口框架;未保存的工作可以阻止重启(AppMain::can_restart)。 | session relaunch |
| 窗口内显示错误 | 编译错误和借用检查器拒绝的编辑会覆盖显示在应用上,并附上文件和行号。崩溃后窗口会保留,并在源码变更后提供 Restart。 | session view open |
| 修复 panic 后重试 | panic 会暂停应用,环上显示“Paused on a panic”;点击后会显示消息及其位置。在 UI 线程上修复代码、保存,然后 retry 会用新代码重新运行该事件。 | panics、locals panic=P1、resume P1 retry |
| 查看代码 | 每个函数的代码版本、O3 备注、与 rustc 并排的机器码,以及飞行记录器。 | versions <fn>、remarks <fn>、asm <fn> vs=rustc、rec save |
| 驱动 UI | 应用的远程控制共享 Relax 的 inspect socket(同一用户,不经过网络)。 | ui find q=<text>、ui act node=<id> press、grab |
除 makepad run 之外的命令都归入 makepad inspect <app>。在 Makepad 的 agent 通道中,经过一整天的运行,一次编辑在到达机器后 3.6 秒(中位数)即被修补,而在冷 JIT 或缓存情况下,p90 为 86 秒。
为 AI agent 而构建
AI agent 感受不到构建缓慢,但每一次构建都要以 token、实际耗时和丢失的上下文为代价。Makepad 依靠众多 AI agent 并行工作来构建其应用,而其开发中枢 devhub 则在热机器上通过 Relax 让每个 agent 的应用或测试运行保持存活。
| 智能体的一次迭代 | 使用 cargo + rustc | 使用 Relax + devhub |
|---|---|---|
| 编辑到达程序 | 重新构建并重新链接:在大型应用上需要数分钟 | 0.1 秒内交付,3.6 秒内完成补丁(中位数) |
| 测试判定 | 重新构建测试二进制文件,然后运行 | 保存后 2.2 秒内返回 92 个测试结果 |
| 查看 UI | 重新构建、重新启动、导航返回 | 实时应用的一帧画面,以 PNG 格式抓取 |
| 新 worker | 冷构建 | 热启动:每台机器都在最新版本处保留热缓存 |
在实践中,九条 agent 通道并行运行,devhub 每小时处理 300 多个构建和测试任务,每天有数十项变更落地,每一项在合并前都要经过 Relax 的检查。
LLVM 仍然胜出的地方
Relax 的稳态代码比 LLVM 慢。同一工作负载在 Relax 下的耗时与其在 rustc 下的耗时之比(越低越好,1.00× = rustc)。
| 工作负载 | x64 对比 stock | x64 对比 native | arm64 对比 stock | arm64 对比 native |
|---|---|---|---|---|
| 文本整形与字形栅格化 | 1.36× | 1.38× | 1.33× | 1.31× |
| 有符号距离场字形生成 | 1.31× | 1.45× | 1.26× | 1.26× |
| DEFLATE 解码 | 1.20× | 1.19× | 1.26× | 1.25× |
| ZIP 归档读取 | 1.22× | 1.26× | 1.11× | 1.10× |
| PNG 解码 | 1.21× | 1.25× | 1.32× | 1.32× |
| Brotli 解码 | 1.29× | 1.26× | 1.27× | 1.28× |
| 矢量地图瓦片解码 | 1.06× | 1.06× | 1.01× | 0.99× |
| 控件树布局 | 1.54× | 1.49× | 1.75× | 1.74× |
| LLM 解码主机路径 | 1.12× | 0.91× | 0.87× | 1.05× |
| Rust 源码分词器 | 1.06× | 1.06× | 1.20× | 1.21× |
| Markdown 解析 | 1.39× | 1.31× | 1.54× | 1.56× |
| HTML 解析 | 1.06× | 0.89× | 1.04× | 1.11× |
| 物理钢琴模型音频 | 1.22× | 1.25× | 1.44× | 1.44× |
| Ogg Vorbis 音频解码 | 1.20× | 1.20× | 1.24× | 1.27× |
| 游戏音频混音器 | 1.03× | 1.16× | 1.17× | 1.18× |
| 脚本虚拟机求值 | 1.11× | 1.09× | 1.12× | 1.13× |
| 实体几何(CSG)布尔运算 | 1.20× | 1.25× | 1.11× | 1.12× |
| 刚体物理 | 0.43× | 1.38× | 1.30× | 1.32× |
| 游戏模拟 tick | 0.60× | 1.26× | 1.25× | 1.25× |
| 关卡遍历(导航网格与行走器巡游) | 0.87× | 0.87× | 0.91× | 0.91× |
| 3D 模型构建 | 2.00× | 2.04× | 1.91× | 1.92× |
| 几何平均(21) | 1.12× | 1.22× | 1.24× | 1.25× |
总体而言,Relax 运行的代码比 rustc 构建的代码更慢:在 21 个工作负载上的几何平均耗时是 x64 上标准 --release 的 1.12 倍、arm64 上的 1.24 倍,是原生 CPU 构建的 1.22 倍和 1.25 倍。Relax 仅在少数工作负载上更快。与标准构建相比,它在 x64 上于刚体物理(0.43×)、游戏模拟 tick(0.60×)和关卡行走(0.87×)上更快,在 arm64 上于 LLM 解码主机路径(0.87×)和关卡行走(0.91×)上更快。x64 上的两项显著优势在与原生构建对比时消失(刚体物理 1.38×,游戏模拟 tick 1.26×):这两项优势是针对通用 CPU 基线取得的。最差的工作负载是 3D 模型构建,耗时约为 rustc 的两倍。
内核微基准测试(30 项):在 x64 上稳态几何平均为标准构建的 1.14 倍、原生构建的 1.24 倍,在 arm64 上约为 1.23 倍。这些数据是暂定的,直到一个热缓存分层升级 bug 被修复:在热 JIT 缓存下,一些热点函数始终不会升级到 O3,因此内核数据可能显得偏慢,属于上界。上述 21 个工作负载的表格不受影响。
x64:8 核桌面 CPU,Linux;arm64:Apple M4 笔记本,macOS。rustc 1.99.1,LLVM 23.1.1。每个数字是 3 轮的中位数,测量于 2026-11-12。标准构建是面向通用 CPU 的 cargo build --release;原生构建是带 -C target-cpu=native 的 --release。Relax 以其默认模式计时,处于分层升级后的稳态。在 x64 上,标准构建和原生构建两列来自同一台机器和同一批二进制文件上的独立运行。在 arm64 上,通用 CPU 已经是 M 系列基线,因此两列仅因噪声而不同。
哪个 --release? 两者都进行了测量:标准构建(cargo build --release,通用 CPU)和原生构建(-C target-cpu=native)。Relax 始终针对其运行的 CPU 进行编译,因此原生构建才是同类对比的列。
差距的来源,来自同一次 3D 打印切片器运行中相同函数的指令级比较:多出的指令而非停顿(1.26 倍的指令数带来 1.15 倍的周期数)。
| 原因 | 额外指令(切片器运行) |
|---|---|
| 比较结果保留为布尔寄存器并重新计算 | ~25 G |
| 寄存器复制(双地址形式,合并未命中) | ~23 G |
| 栈流量:循环状态保存在栈槽中;可分配寄存器为 12 个,而 rustc 为 15 个 | ~22 G |
| 额外的循环控制和检查分支,不变量测试留在循环内部 | ~13 G |
| 加载未折叠进比较、i128 算术、重新物化的常量、更小的项 | ~33 G |
| 生成代码之外的运行时(memcpy、分配器包装、TLS 访问、存根) | ~40 G |
| 无循环向量化,展开较浅 | 数值内核差距的大部分 |
内联并不是原因:Relax 的内联比 rustc 更多。每一行都是一个工作项。
Relax 用于开发迭代:发布构建使用 rustc。
架构
Relax 在单个进程中掌控从源码字节到可执行内存的整个流水线。它由一系列分层组成,每一层都是独立的 crate,只构建在下方各层之上;各层并行编译,上层的变化永远不会重新构建下方各层。
| Crate | 路径 | 职责 | 在 rustc 世界中的对应物 |
|---|---|---|---|
relax-lex | libs/relax/lex | Rust(所有 edition)的词法分析器和 token 模型,也是过程宏所看到的那个。 | 词法分析器和 proc_macro |
relax-base | libs/relax/base | 哈希、内存核算与 arena、语法树、cfg、清单文件、git 读取路径和源码挂载。 | Cargo 和 rustc 共享基础设施的一部分 |
relax-session | libs/relax/session | 一次编译的输入及其稳定哈希:所有缓存赖以构建的键。 | session 和增量键 |
relax-front | libs/relax/front | 解析、crate 图、名称解析和宏展开。 | 解析器、解析器(resolver)、宏展开 |
relax-types | libs/relax/types | 类型、impl、类型检查、布局、常量求值和借用检查器。 | 类型检查器、借用检查器、const eval |
relax-codegen | libs/relax/codegen | 基于 RIR(Relax IR)的中端:O1 和 O3 优化器以及线程化解释器。 | MIR 优化和 LLVM 的优化器 |
relax-backend | libs/relax/backend | 寄存器分配、x86-64 和 AArch64 机器码、C ABI 规划器和可执行内存。 | LLVM 的代码生成器和链接器 |
relax-compiler | libs/relax/compiler | JIT 运行时和 std:惰性编译、分层、实时编辑、缓存、proc-macro 宿主、测试运行器和 relax inspect。 | Cargo、rustc 驱动、测试框架和操作系统加载器 |
relax-native-macros | libs/relax/native_macros | 原生链接的过程宏,会对照其源码进行校验。 | proc-macro dylib |
| attribute | libs/relax/attr | #[relax(O2)] / #[relax(O3)]:优化级别属性;在 rustc 下为空操作。 | 按 profile 设置的 opt-level |
relax_api | libs/relax_api | #[relax_api]:原生 crate 向 JIT 代码提供的接口,以及按线程的堆路由。 | (无对应物:一切都是原生的) |
函数的生命周期。 在 main 之前,程序自身的 crate 会进行类型检查。函数的首次调用会编译它(解释器或 O1);当它变热时,会在后台以 O3 重新编译并替换。当你保存对它的修改时,Relax 会重新检查变更内容,重新编译并发布新代码;旧代码仍然有效,下一次调用会运行新函数。
设计规则。 永不陈旧: 每个缓存结果都以其输入的哈希为键。代码只增不减: 已发布的机器码在仍可能位于栈上时绝不会被释放或重写,因此交换是安全的,而原生热重载 hack 则不然。默认惰性: Relax 只编译你调用的内容,而非你可能调用的所有内容。可检查: 每个阶段都会报告它正在做什么,因此停滞可以从其记录中得到解释。
仓库布局
Cargo.toml the workspace
libs/relax/
lex/ base/ session/ front/ types/ front end and type system
codegen/ backend/ middle end and machine code
compiler/ runtime, std (compiler/std), 96 end-to-end test packages
attr/ native_macros/ #[relax(..)] and native proc macros
KNOWN_GAPS.md what isn't supported yet
libs/relax_api/ #[relax_api] and per-thread heap routing
tools/run/runner/ relax, the command-line runner
libs/ platform/ tools/ widgets/ the small support crates Relax links: token model,
serialisation, remote inspection, profiling, and the
native proc macros it checks against their source编译器是 relax_compiler 库(cargo build --release -p relax-compiler);relax(tools/run/runner)是它的命令行运行器。构建它需要稳定的 Rust 工具链,且无需来自 crates.io 的任何内容。
许可证
MIT,参见 LICENSE。Relax 编译的 std 源码带有各自的声明;参见 libs/relax/compiler/std/CREDITS.md 及其旁边的许可证文件。