跳到正文
Hacker News· adamnemecek·· 1 天前AI 评分73

Makepad 发布 Rust JIT 编译器 Relax,边运行边编译以减少构建等待

Relax: JIT compiler for Rust to reduce build times

AI 导读

Makepad 项目发布 Relax,一个用 Rust 编写的 Rust 实时编译器与 JIT 运行时,直接从源码运行 Cargo 包,不经过 rustc、LLVM 和链接器,函数首次调用时编译、热函数在后台升级到 O3,保存文件即可把改动热补丁进运行中的进程并保留状态。

正文 · AI 翻译

这是该页面的纯 HTML 全文,供没有 JavaScript 的读者阅读。同样的文本也出现在 README 和 纯 HTML 页面 中。

无需等待的 Rust。

rustc 先构建你的程序,然后你再运行它。Relax 在构建程序的同时运行它。

Relax 是一个用 Rust 编写的实时编译器与 JIT 运行时,专为 Rust 打造。它直接从源码运行 Cargo 包,全程无需 rustc、LLVM 或链接器:它在每个函数首次被调用时进行编译,在程序运行期间优化热点函数,并将你的修改热补丁到正在运行的进程中。

Relax 与 cargo、rustc 的对比

cargo + rustcRelax
大型 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! 与过程宏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
窗口中的错误编译错误和借用检查器拒绝的编辑会显示在应用之上,并附带文件和行号。崩溃后窗口会保留,并在源代码更改后提供重启选项。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 对比 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×
脚本 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×
游戏模拟 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 倍。这些数据是暂定的,直到热缓存 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-lexlibs/relax/lexRust(所有版本)的词法分析器和 token 模型,也是过程宏所看到的那个。词法分析器和 proc_macro
relax-baselibs/relax/base哈希、内存核算与 arena、语法树、cfg、清单文件、git 读取路径和源码挂载。Cargo 和 rustc 共享基础设施的组成部分
relax-sessionlibs/relax/session一次编译的输入及其稳定哈希:每个缓存都建立在这些键之上。session 和增量键
relax-frontlibs/relax/front解析、crate 图、名称解析和宏展开。解析器、解析器、宏展开
relax-typeslibs/relax/types类型、impl、类型检查、布局、常量求值和借用检查器。类型检查器、借用检查器、常量求值
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:惰性编译、分层、实时编辑、缓存、过程宏宿主、测试运行器和 relax inspect。Cargo、rustc 驱动、测试框架和操作系统加载器
relax-native-macroslibs/relax/native_macros原生链接的过程宏,会对照其源码进行检查。过程宏动态库
attributelibs/relax/attr#[relax(O2)] / #[relax(O3)]:优化级别属性;在 rustc 下为空操作。按 profile 的 opt-level
relax_apilibs/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 智能体而构建

      持续保持热度的循环

      Makepad

      实践之中

      速度

      Relax 与 rustc,在真实工作负载上。

      展示数字

      诚实的数字

      LLVM 仍然胜出之处。

      分层与实时补丁

      一个函数的一生。

      来源:Hacker News · makepad.github.io