Makepad 发布 Rust JIT 编译器 Relax,边运行边编译以减少构建等待
Relax: JIT compiler for Rust to reduce build times
Makepad 项目发布 Relax,一个用 Rust 编写的 Rust 实时编译器与 JIT 运行时,直接从源码运行 Cargo 包,不经过 rustc、LLVM 和链接器,函数首次调用时编译、热函数在后台升级到 O3,保存文件即可把改动热补丁进运行中的进程并保留状态。
这是该页面的纯 HTML 全文,供没有 JavaScript 的读者阅读。同样的文本也出现在 README 和 纯 HTML 页面 中。
无需等待的 Rust。
rustc 先构建你的程序,然后你再运行它。Relax 在构建程序的同时运行它。
Relax 是一个用 Rust 编写的实时编译器与 JIT 运行时,专为 Rust 打造。它直接从源码运行 Cargo 包,全程无需 rustc、LLVM 或链接器:它在每个函数首次被调用时进行编译,在程序运行期间优化热点函数,并将你的修改热补丁到正在运行的进程中。
Relax 与 cargo、rustc 的对比
| cargo + rustc | Relax | |
|---|---|---|
| 大型 3D 游戏,全新检出 → 首帧 | 构建约 3 分钟,然后启动 | 11 秒,在 M4 MacBook 上,直接从源码运行 |
| 同一游戏,第二次运行 → 首帧 | 启动已构建的二进制文件 | 1.15 秒,在 M4 MacBook 上,无需重新编译 |
| 小型应用 → 首帧 | 先构建它 | 冷启动 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 库:43.8 万行,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/relax中的代码缓存在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 的 std,因此请将 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/relax 中(relax clean 会将其删除),因此每个新包都从冷启动开始。
网络。没有依赖的包(如 hello)不需要网络。对于 crates.io 依赖,如果 ~/.cargo 的注册表中存在该包,Relax 会从那里获取;否则会自行从 crates.io 下载,并对照索引的 SHA-256 进行校验。使用 RELAX_OFFLINE=1 或 cargo 的 CARGO_NET_OFFLINE 时,它从不下载。relax fetch <git-url>(tools/run/runner/src/fetch.rs)将 git 仓库存储为 ~/.relax 下的单个文件。
代价:一套新的 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 |
| 窗口中的错误 | 编译错误和借用检查器拒绝的编辑会显示在应用之上,并附带文件和行号。崩溃后窗口会保留,并在源代码更改后提供重启选项。 | 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 套接字(同一用户,网络上无暴露)。 | 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 的应用或测试运行存活。
| 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× |
| 脚本 VM 求值 | 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 倍。这些数据是暂定的,直到热缓存 tier-up 缺陷修复为止:在热 JIT 缓存下,一些热点函数永远不会 tier up 到 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 以其默认模式计时,处于 tier-up 后的稳态。在 x64 上,原版和原生两列来自同一台机器和二进制文件上的独立运行。在 arm64 上,通用 CPU 已经是 M 系列基线,因此两列仅因噪声而不同。
哪个 --release? 两者都进行了测量:原版(cargo build --release,通用 CPU)和原生(-C target-cpu=native)。Relax 始终针对其运行的 CPU 进行编译,因此原生是对等比较的列。
差距的来源,来自一次 3D 打印切片器运行中相同函数的指令级比较:多出的指令而非停顿(1.26 倍的指令数带来 1.15 倍的周期数)。
| 原因 | 多出的指令(切片器运行) |
|---|---|
| 比较结果保留为 bool 寄存器并重新计算 | ~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(所有版本)的词法分析器和 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 图、名称解析和宏展开。 | 解析器、解析器、宏展开 |
relax-types | libs/relax/types | 类型、impl、类型检查、布局、常量求值和借用检查器。 | 类型检查器、借用检查器、常量求值 |
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:惰性编译、分层、实时编辑、缓存、过程宏宿主、测试运行器和 relax inspect。 | Cargo、rustc 驱动、测试框架和操作系统加载器 |
relax-native-macros | libs/relax/native_macros | 原生链接的过程宏,会对照其源码进行检查。 | 过程宏动态库 |
| 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 会重新检查变更内容,重新编译并发布新代码;旧代码仍然有效,下一次调用会运行新函数。
设计规则。 永不陈旧: 每个缓存结果都以其输入的哈希为键。代码只增不减: 已发布的机器码只要还可能位于栈上,就绝不会被释放或重写,因此这里的替换是安全的,而原生热重载的取巧做法则不然。默认惰性: 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 及其旁边的许可证文件。
一个用 Rust 编写的、面向 Rust 的实时编译器和 JIT 运行时
Rust 无需 等待。
rustc 构建你的程序,然后你运行它。 Relax 在构建程序的同时运行它。
Relax 直接从源码运行 Cargo 包,全程无需 rustc、LLVM 或链接器。它在函数首次被调用时编译它,在程序运行时优化热点函数,并将你的编辑补丁进正在运行的进程。
记分板
Relax 对比 rustc
Relax
rustc + Cargo
权衡取舍
JIT 让你付出什么。
收益
JIT 给你什么。
保存即构建
运行中的程序
- 自启动以来的帧数
- 补丁
- 重启
试一试
构建它,运行一个包。
它经历了什么
一条新的 Rust 工具链,从源码字节到机器码。
这项工作 · 在常规工具链中 · 在 Relax 中
Makepad 如何使用 Relax
makepad run 中的 Relax
为 AI 智能体而构建
持续保持热度的循环
实践之中
速度
Relax 与 rustc,在真实工作负载上。
展示数字
诚实的数字
LLVM 仍然胜出之处。
分层与实时补丁
一个函数的一生。
来源:Hacker News · makepad.github.io