Relax 发布 Rust JIT 编译器 构建提速至 11 秒

原标题:Relax: JIT compiler for Rust to reduce build times
78AI 研判分

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 + rustcRelax
大型 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 运行一个新包:

BASH
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! 和过程宏rustcrelax-lex、relax-front
名称解析、trait 求解、类型推断、借用检查、常量求值rustcrelax-types
优化:内联、常量折叠、循环、范围rustc 的 MIR pass + LLVMrelax-codegen(O1 和 O3)
寄存器分配、生成 x86-64 和 AArch64 机器码LLVMrelax-backend
链接与加载系统链接器和加载器relax-compiler,直接写入可执行内存
读取清单并解析 crate 依赖图Cargorelax-base
构建 core、alloc 和 std随附预编译版本由 Relax 从源码编译(134k 行)
cargo + rustcRelax
---------
编译整个 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 对比 stockx64 对比 nativearm64 对比 stockarm64 对比 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×
游戏模拟 tick0.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-lexlibs/relax/lexRust(所有 edition)的词法分析器和 token 模型,也是过程宏所看到的那个。词法分析器和 proc_macro
relax-baselibs/relax/base哈希、内存核算与 arena、语法树、cfg、清单文件、git 读取路径和源码挂载。Cargo 和 rustc 共享基础设施的一部分
relax-sessionlibs/relax/session一次编译的输入及其稳定哈希:所有缓存赖以构建的键。session 和增量键
relax-frontlibs/relax/front解析、crate 图、名称解析和宏展开。解析器、解析器(resolver)、宏展开
relax-typeslibs/relax/types类型、impl、类型检查、布局、常量求值和借用检查器。类型检查器、借用检查器、const eval
relax-codegenlibs/relax/codegen基于 RIR(Relax IR)的中端:O1 和 O3 优化器以及线程化解释器。MIR 优化和 LLVM 的优化器
relax-backendlibs/relax/backend寄存器分配、x86-64 和 AArch64 机器码、C ABI 规划器和可执行内存。LLVM 的代码生成器和链接器
relax-compilerlibs/relax/compilerJIT 运行时和 std:惰性编译、分层、实时编辑、缓存、proc-macro 宿主、测试运行器和 relax inspect。Cargo、rustc 驱动、测试框架和操作系统加载器
relax-native-macroslibs/relax/native_macros原生链接的过程宏,会对照其源码进行校验。proc-macro dylib
attributelibs/relax/attr#[relax(O2)] / #[relax(O3)]:优化级别属性;在 rustc 下为空操作。按 profile 设置的 opt-level
relax_apilibs/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 及其旁边的许可证文件。

Relax 发布 Rust JIT 编译器 构建提速至 11 秒 | Today for AI