News Hacker|极客洞察

132 74 天前 github.com
🤔为 Postgres 提升 JIT:编译延迟、缓存与进程隔离问题
真的要每次都重编译 JIT 然后抱怨慢吗?

🎯 讨论背景

讨论围绕为 PostgreSQL 改善 JIT 性能的可行路径展开:当前 Postgres 常用的 LLVM‑based JIT 在编译时延上存在显著成本,使短时低延迟查询难以受益。评论里有人建议 AOT、持久缓存已编译代码或替换更快的后端(如 pg_jitter),但也指出 Postgres 的 process‑per‑connection 架构难以跨会话共享代码。社区还讨论了 tiered JIT / 后台编译、为特定 schema 预编译二进制(arXiv 研究)以及像 Umbra 和 Hyper 这样的 JIT‑重型数据库示例;关于用 LLM 生成编译器的想法也被提出,但因延迟与确定性问题遭到质疑。

📌 讨论焦点

LLVM 编译慢与缓存/AOT

评论指出 PostgreSQL 默认的 JIT 提供者基于 LLVM,编译延迟常为几十到数百毫秒,这让 JIT 只对很重的 OLAP 查询有意义。多位评论者建议采用 ahead‑of‑time (AOT) 编译或持久化缓存 JIT 产物以避免每次执行都付出高额编译成本,并以 PyPy 的 5x 加速为例说明改进解释器/编译器能带来显著收益。有人提出更激进的方案——为特定 schema 预编译整个 DBMS 二进制,并引用了相关 arXiv 研究作为支撑。具体调优也被提及:Postgres 默认 jit_above_cost 为 100000,对 LLVM 合理,但对更快后端(如 pg_jitter)应把该值降到约 200 到几千以更早触发编译。

[来源1] [来源2] [来源3] [来源4] [来源5]

进程模型与跨进程共享限制

大量评论集中在 Postgres 的 process‑per‑connection 架构会阻碍已生成查询计划或 JIT 代码在进程间共享,导致每个连接重复规划和编译,增加延迟。评论指出编译产物常内嵌运行时专属指针和优化假设,使得跨进程重用困难且需额外修正,简单把机器码写成共享 .so 也会在高并发下触发文件系统瓶颈。研究被引用说明跨应用共享 JIT 缓存遇到的挑战(运行时上下文、内联与指针依赖等),并把 Postgres 与运行单进程多线程模式的数据库(如 MSSQL)作对比,后者更容易实现跨会话缓存与复用。

[来源1] [来源2] [来源3] [来源4] [来源5]

Prepared statements 与库端自动管理

多数人指出使用 prepared statement 可以在连接层预编译并复用计划,从而避免重复规划/编译的开销,但 Postgres 的 PREPARE 是 per‑connection,因此在连接池或短连接场景下受限。作为权宜之计,客户端或驱动库可以自动管理 prepared statements;例如评论提到 Go 的 pgx 和 Python 的 psycopg3 会替应用程序自动准备并缓存语句。评论还比较了其他 RDBMS 基于语句文本的全局 plan cache 与早期通过存储过程来获得缓存的做法,说明 Postgres 在会话间共享方面与“老牌”数据库的差异。

[来源1] [来源2] [来源3] [来源4] [来源5]

JIT 策略与替代方案(tiered JIT、后台编译、示例引擎)

评论提出多种工程策略以兼顾初始延迟与长期性能:典型思路是 tiered JIT(解释器→基线编译→优化编译),或在主线程解释执行同时在后台线程并行编译,编译完毕后切换到生成的机器码以减少延迟峰值。实际数据库案例被引用:Umbra 和 Salesforce 的 Hyper 等以 JIT 为核心、能在低启动延迟下运行的引擎显示不一定要全靠缓存 IR/代码也能获得高性能。对 Postgres 的改进路径包括替换更快的 JIT 后端(如 pg_jitter)并调低 jit_above_cost,以及探索为特定 schema 生成 AOT 二进制等更激进方案。

[来源1] [来源2] [来源3] [来源4] [来源5]

工作负载决定是否受益(CPU‑bound、jsonb 与并行性问题)

讨论强调是否值得 JIT 很大程度取决于工作负载:许多实际系统活跃数据集多数驻留内存(命中率常在 90%+),在这种情况下查询往往由 I/O 转为 CPU 绑定,减少 CPU 开销变得关键。评论举例 jsonb 操作极易成为 CPU 瓶颈,实际工程上会把 JSON 做转换或为 JSON 路径建索引来显著加速查询。还有人抱怨 Postgres 对单查询的多核利用不足(常只用单核),相比之下列式/并行引擎如 ClickHouse 在单查询并行度上更能发挥硬件优势;即便读取走缓存也存在缓冲区锁等 CPU 成本。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6]

用 LLM/AI 生成编译器的可行性与限制

有人建议用 LLM 来合成定制编译器或辅助生成后端,但评论指出两个明显障碍:LLM 推理延迟通常以秒计,而这里目标是以十毫秒计的快速编译;另外数据库场景强烈要求确定性,而 LLM 在实践中因批处理、并行与浮点实现等因素可能出现非确定性。反方认为非确定性并非“天生”不可控,且可以把 LLM 用于设计/生成编译器工具链而不是每次即时编译。总体共识是 LLM 现在还不适合替代低延迟、可确定性的 JIT 执行路径,但作为辅助工具有讨论价值。

[来源1] [来源2] [来源3] [来源4]

📚 术语解释

JIT (Just‑In‑Time,即时编译): 在运行时把查询表达式或执行路径编译为机器码以提高执行速度,但会引入编译延迟和生成代码的缓存/共享复杂性;Postgres 默认 JIT 使用 LLVM,延迟敏感场景受影响。

LLVM: 一套通用的编译器基础设施与优化框架,Postgres 的内置 JIT 常基于 LLVM 实现,能产出高性能机器码但编译时间较长。

prepared statement (PREPARE,预准备语句): 在连接级别预先准备并缓存 SQL 的解析/执行计划以避免重复规划;在 Postgres 中 PREPARE 是 per‑connection(每连接)缓存,短连接/连接池场景受限。

query plan / plan cache: 查询规划器为 SQL 生成的执行步骤序列;一些数据库支持基于语句文本的全局 plan cache 与已编译代码复用,而 Postgres 在默认进程隔离下难以跨会话共享这些缓存。

process‑per‑connection(每连接一个进程): Postgres 的服务端架构模式:每个客户端连接由独立操作系统进程处理,这提高隔离性但使得在进程间共享可执行代码或序列化计划更困难。

tiered JIT: 多层 JIT 策略:先用解释器或基线编译器快速执行,再在后台生成更优化的机器码并切换,以兼顾初始延迟与长期性能(常见于 JS VM)。

pg_jitter: 评论中提到的替代 Postgres JIT 提供者示例,编译更快但需要调整参数(例如将 jit_above_cost 从默认 100000 降到数百或数千)以便在短查询场景触发编译。