pgrust 技术解读:5 微秒 JIT 编译器如何让 Rust 重写的 Postgres 快过 ClickHouse

数据库圈有一条不成文的分工:查询引擎的执行层要么逐行解释执行,要么把查询表达式交给 LLVM 或先生成 C 代码再调用系统编译器。2026 年 4 月出现在 GitHub 上的开源项目 pgrust 选择了第三条路,作者 malisper 为这套 Rust 重写的 PostgreSQL 自研了一个 JIT 编译器,编译一条查询只要约 5 微秒,快到可以对每一条 SQL 都现场编译,包括那些只跑一次的短查询。作者在博文《JIT Compiling Code in 5μs》里公开了完整实现思路,这篇文章同时登上 Hacker News 与 Lobsters 首页。

pgrust GitHub 仓库

为什么生产数据库没有自研 JIT

JIT 编译指在程序运行时现场生成机器码,用编译后的代码替换解释执行,典型收益在 2 到 5 倍,有时更高。它最适合"运行时才拿到代码"的场景,SQL 查询正是典型:数据库在收到查询前不知道用户要执行什么。

但翻开主流数据库的实现,自研 JIT 几乎绝迹。作者给出的判断是:现存的数据库要么用 LLVM 做即时编译,要么生成 C/C++ 代码再走离线编译,两条路都受限于编译耗时,只能覆盖一部分查询。PostgreSQL 自带的 LLVM JIT 就属于此类,代价高到默认只在分析型长查询上启用。直接以汇编为目标写一个编译器,历来被视作需要深厚底层功力的黑魔法。作者的坦白很有代表性:写 JIT 之前,他写汇编的经验只限于打过一场 microcorruption CTF,从没真正手写过汇编。

变化来自 AI 辅助编程。作者把 JIT 编译器的整体设计交给编码 Agent,由 Agent 处理指令选择与编码细节,最终得到了一个编译耗时约 5 微秒的 copy-and-patch 编译器。这个数字意味着编译成本低到可以忽略,pgrust 因此能对每一条 SQL 做 JIT,无须挑选"值得编译"的子集。

copy-and-patch:用模板填空生成机器码

pgrust JIT 采用 copy-and-patch 变体。思路是为每种操作预先准备好汇编模板,术语叫 stencil(镂花模板)。编译一个操作时,取出对应的 stencil,把操作数、跳转目标等具体数值填进指令的立即数字段,再把填好的模板依次拼接,就得到一段可执行的机器码。整个过程没有指令选择、没有寄存器分配,编译快是结构性的。

作者用一个支持字面量和星号重复的正则引擎演示了完整流程。以正则 b(an)* 为例,目标平台是 macOS ARM64。寄存器约定很紧凑:x0 存当前匹配位置兼返回值,x1 存回溯栈栈顶,x2 存栈底用于判断栈空,x9 做临时变量。输入字符串以 NUL 字节结尾,这样字符比较在串尾自动失败,全程无须长度检查。

生成的机器码由六类 stencil 拼装:prologue 初始化回溯栈;char 模板做单字节比较,内嵌 ldrb/cmp/b.ne/add 四条指令;split 模板处理星号重复,把循环出口地址和当前位置压栈,供回溯使用;jmp 模板回跳循环头;match 模板检查是否抵达串尾并返回 1;fail 模板弹栈回溯,栈空则返回 0。填空靠几个十行内的辅助函数:条件跳转偏移写入指令的 5..24 位,无条件跳转写入 0..26 位,64 位绝对地址拆成四段 16 位,用 movz 加三条 movk 逐段拼装。

从 AST 到机器码的驱动代码是一个递归下降的 emitter。它先递归计算每个节点编译后的指令字数,据此确定回溯出口的绝对地址,再递归填充模板。整个过程约百行 Rust。

让操作系统放行:mmap 与 W^X

生成的指令需要放进可执行内存。pgrust JIT 用 mmap 申请一块 PROT_READ | PROT_WRITE | PROT_EXECMAP_JIT 匿名内存,把指令拷进去,然后转成普通函数指针调用。macOS 的 W^X 机制要求同一页内存不可同时可写可执行,所以拷贝前后分别调用 pthread_jit_write_protect_np(0)(1) 切换写保护,再调用 sys_icache_invalidate 刷新指令缓存。回溯栈预分配 4096 帧,用完即 munmap 释放。

这个玩具引擎的基准结果(原文数据,输入长度 / 解释器 / JIT / 手写):

输入长度解释器JIT手写JIT 相对解释器
945 ns3.8 ns3.8 ns11.7x
33103 ns7.9 ns10.5 ns13.0x
129597 ns30 ns32 ns19.7x
5131,955 ns126 ns120 ns15.5x
2,0498,301 ns470 ns393 ns17.7x

JIT 版与针对该正则手写的特化代码基本持平,五个长度档里互有胜负,相对解释器稳定在 11 到 20 倍。这正是 copy-and-patch 的卖点:通用引擎跑出特化代码的速度。

JIT 在 pgrust 里处在什么位置

pgrust 定位为"如果 Postgres 在 2026 年重写一遍会是什么样",与 PostgreSQL 线协议和 SQL 方言兼容,全部代码用 Rust 实现。README 给出的进度:通过 PostgreSQL 回归测试套件全部 46,066 个测试;版本 v0.2;AGPL-3.0 许可;GitHub 上 4,634 star、173 fork(2026 年 8 月 23 日 GitHub API 数据)。

性能方面,README 记录了在 AWS c8g.4xlarge(Graviton4)上对比 PostgreSQL 18.3 的结果:ClickBench 综合分领先 ClickHouse 18.5%,相对 PostgreSQL 快数百倍;sysbench-oltp 只读负载在 300GB 规模下吞吐高 30%。这些跑分由《PostgreSQL 9.0 High Performance》作者 Greg Smith 独立复核。作者同时披露了两点诚实 caveat:此前宣称的 50% OLTP 优势在裸 EC2 上实测为 30%(Kubernetes 环境下为 50-60%,原因未定位);发布的二进制是通用构建,跑分数字来自 -Ctarget-cpu=neoverse-v2 的 Graviton4 专用构建,用户直接下载难以复现。

查询执行层的一篇配套博文拆解了另一部分提速来源。对 5 亿行浮点数求和,PostgreSQL 18.4 耗时约 20 秒,等价 Rust for 循环 358 毫秒。把 Postgres 的 Volcano 逐行模型逐级优化:加 1024 行批处理降到 480 毫秒;算子融合降到 358 毫秒;NEON SIMD 多车道累加降到 135 毫秒,累计相对 Volcano 基线约 10 倍。作者指出,算子融合相当于为已知查询提前手写理想代码,JIT 把这种"作弊"推广到任意查询,这正是 5 微秒编译器的用武之地:每个查询都能拿到为自己生成的融合代码。

可信度工程:回归测试之外

对一个重写项目,"通过测试不等于正确"是绕不开的质疑。README 单列了三条应对措施。其一,PostgreSQL 约 3,000 个用户可见函数中已有 1,000 个用 Kani 形式化验证工具证明行为与 Postgres 一致,过程中发现 12 处行为分歧,其中 4 处最终定性为 Postgres 自身的 bug。其二,与 Antithesis 合作做模拟测试。其三,对 pgrust 与 Postgres 做激进的差分模糊测试。

现状边界同样写得很清楚:pgrust 尚未达到生产可用,别把重要数据放进去;现有 PostgreSQL 扩展无法运行(尚无稳定扩展 ABI,PL/Python、PL/Perl、PL/Tcl 未移植);JIT 编译器目前只针对 Graviton4,其他平台能跑但性能另论。

代码曾经就是最难的部分

作者在文末回应了"AI 帮不上忙,因为代码从来不是最难的部分"这个流行说法。他的观察相反:在某些领域,写代码本身就是最难的部分,JIT 编译器是典型。自研 JIT 的稀缺说明过去做这件事的难度与收益不成正比,LLM 把门槛拉低之后,这类原本不值得做的底层组件开始变得可行。pgrust 的整个立项就建立在这个判断上:数据库历来是最难写的软件之一,也因此被历史形态锁死,当写底层的成本下降,新一代软件可以更激进地重新设计。

这个判断的适用范围有待时间检验,但 5 微秒的编译器已经跑起来了,浏览器里就能试:官网 pgrust.com 提供编译到 WebAssembly 的完整服务端。


来源: