9.1 从逐行解释到表达式 JIT
查询执行时间可以花在 I/O、hash/sort、tuple decoding,也可以花在“怎样执行表达式”本身。只有当解释调度和表达式求值占到足够比例,bytecode 或 JIT 才可能收回编译成本。把所有慢查询都归因于虚函数,通常会优化错层。
本课目标
- 识别 iterator/表达式解释的 CPU 开销来源;
- 区分 AST walk、bytecode VM、specialized function 与 JIT;
- 理解 PostgreSQL JIT 的覆盖边界;
- 计算 compile cost 与 execution savings 的 break-even;
- 设计能区分 I/O-bound 与 CPU-bound 的实验。
解释执行的两个层次
Operator dispatch
逐行 iterator 接口片段(partial,Row 由执行器定义):
struct Operator {
virtual ~Operator() = default;
virtual bool next(Row& output) = 0;
};一行经过 Scan → Filter → Project,可能触发多次 indirect calls、state checks 和 branches。
Expression evaluation
predicate 也可能以 tree walk 解释:
AND
├─ greater_than
│ ├─ column(value_cents)
│ └─ constant(50000)
└─ equals
├─ column(type_id)
└─ constant(1)每行遍历 nodes、dispatch opcode/function、检查 NULL/type。即使 operator 已批处理,复杂 expression interpreter 仍有成本。
不要固定宣称“每次虚调用 10 ns”
indirect branch 成本依赖 CPU、预测历史、inline、cache、编译器、security mitigations 和 surrounding work。query 中的 cache miss、string compare、hash 和 decompression 往往比一次 call 贵得多。
正确测量:
rows/batches processed
cycles, instructions, IPC
branch misses
L1/LLC misses
expression time vs operator time
I/O wait
JIT generation time用 perf 等 profiler 的 production 权限和 overhead 要评估;不要用“行数 × 猜测纳秒”作为事实。
路径一:AST/tree interpreter
优点:实现简单、动态类型/函数扩展方便、planning startup 低。缺点:每值重复 node traversal 和 dispatch。
适合 short query、小数据、复杂 control flow 或 compile cache 命中率低的场景。
路径二:Bytecode VM
把 expression tree lowering 成线性 instructions:
LOAD_COLUMN value_cents
LOAD_CONST 50000
GREATER_THAN_INT64
LOAD_COLUMN type_id
LOAD_CONST 1
EQUALS_INT32
AND_3VL
RETURNVM loop 比 pointer-rich AST 更连续,可以用 switch/direct-threaded dispatch,并在 lowering 时常量折叠、选择 typed opcodes。它仍在解释,但 startup 小于 native JIT,常作为中间层。
SQL NULL 需要 three-valued logic;AND_3VL 不能退化为普通 boolean AND。
路径三:Specialization/template
engine 可以为常见类型与算子组合使用预编译模板或函数指针。下面只有接口声明,依赖 <cstddef>、<cstdint> 和具体实现:
template <typename T>
std::size_t select_greater_than(
const T* values,
const std::uint64_t* validity,
std::size_t size,
T threshold,
std::uint32_t* selected);避免 runtime type switch,但组合过多会扩大 binary/code cache,dynamic UDF/collation 仍需 generic path。
路径四:JIT native code
JIT 根据 bound types、constants、NULLability 与 expression tree 生成 machine code:
for each tuple/batch:
decode known columns
if valid(value) and value > 50000
and valid(type) and type == 1:
emit/probe/aggregate潜在收益:
- 消除 expression-node dispatch;
- inline operators/functions;
- constant folding/dead branch removal;
- specialize NULL/no-NULL path;
- expose loops to compiler optimization。
成本:IR construction、optimization、machine-code emission、memory protection、instruction-cache/code-size、cache key 与 invalidation。
PostgreSQL JIT 的真实范围
PostgreSQL 的 LLVM JIT 主要加速:
- expression evaluation;
- tuple deforming(把 on-page tuple 拆成 Datums/attributes);
- function inlining 与 LLVM optimization(达到相应 cost thresholds 时)。
它不是把整个 physical plan 和每个 node 都融合成一个 native function。scan/join/sort framework 仍由 executor 驱动。
EXPLAIN (ANALYZE, BUFFERS, SETTINGS, SUMMARY ON)
SELECT SUM(value_cents * quantity)
FROM line_items
WHERE status = 'settled';这是 PostgreSQL 计划片段,需已有 line_items(value_cents, quantity, status) 表。JIT 开关开启、实现可用且估算成本达到阈值时,计划才会出现相应 functions、options 和 timing;仅 jit=on 不保证本次查询启用 JIT。PostgreSQL JIT 决策
不要在共享 production session 为演示随意 SET jit_above_cost。先在隔离环境比较 JIT on/off,并记录当前 server defaults/settings。
Break-even
设:
- compile/generation cost
C; - interpreted per-row cost
I; - compiled per-row cost
J; - processed rows
N。
JIT 有收益的必要近似:
C + N×J < N×I
N > C / (I - J)此推导要求 I > J;若编译后的每行成本并未下降,正的编译成本就无法靠增加行数收回。假设 C=10 ms、I-J=50 ns,则需超过 200000 行才有净收益;这是给定参数的计算,不是硬件测量。
但 I-J 会受 expression、cache、vectorization 影响;plan 实际 rows 也可能偏离 estimate。固定经验“查询 >1 秒就开、<50 ms 就关”没有跨系统依据。
只有实际复用生成代码的系统,才能跨查询摊薄 C。PostgreSQL 的预备语句缓存计划,不代表缓存 LLVM 生成函数;其 JIT 实现说明 指出生成函数尚不能跨执行缓存。重复测量时仍要分别记录每次 compilation 与 execution。
JIT 可能更慢的场景
- OLTP point query,execution 比 compile 还短;
- I/O/lock/network-bound;
- expression 很简单,compiler 无可消除工作;
- rows 很少或 predicate 早停;
- code cache churn、instruction-cache pressure;
- many concurrent compiles 争抢 CPU/memory;
- generated code 太 specialized,参数变化频繁。
一个诊断实验
在复制/隔离数据上:
- 选 CPU-heavy、rows 足够多、无 lock wait 的 query;
- 预热或明确报告 cold cache;
- 保存
EXPLAIN (ANALYZE, BUFFERS, SETTINGS); - JIT off/on 各重复多次,区分首次、后续执行和确有证据的代码复用;
- 记录 generation、execution、CPU counters、result hash;
- 确认结果与 floating/overflow semantics 一致;
- 同时测试代表 parameters,而不是只用一个高收益 value。
本课小结
查询编译先解决表达式解释与 tuple decoding 的 CPU 税。bytecode、templates 和 JIT 是不同 startup/steady-state 取舍。下一课把范围扩大到 operator pipelines,解释 push-based code generation 如何跨算子融合循环,以及为什么 sort/hash-build 仍会打断 pipeline。