跳到内容

9.2 Pipeline、Push 执行与查询代码生成

表达式 JIT 仍保留 operator boundary。更激进的 query compilation 让 operators 通过 produce/consume 生成一个 pipeline loop:scan 产生 tuple/batch,filter 直接判断,projection 直接计算,sink 直接写入 hash table 或 result。

Pull、push 与 compiled pipeline

Pull iterator

parent 向 child 请求下一行/批:

text
root.next()
  -> filter.next()
     -> scan.next()

Push execution

source 主动把 batch 推给 downstream consumer:

text
scan -> filter -> projection -> sink

push 不自动等于 native code generation。DuckDB 当前文档描述的是 push-based vectorized model,DataChunks 在 physical operators 间流动;这与“整条查询编译为 LLVM machine code”是两件事。

Compiled produce/consume

operator 参与生成共同 loop:

cpp
for (const auto& row : scan(table)) {
    if (row.level >= 30) {
        hash_table.insert(row.adventurer_id, row.name);
    }
}

上面是生成结果的概念代码,不是某数据库 source API。

Pipeline breaker

pipeline 可以在不要求看完全部 input 的 operators 间 streaming。以下状态通常形成边界:

  • hash join build:probe 前需建表;
  • full sort:输出首行前需形成/合并 runs;
  • hash aggregate:最终 groups 需要完整/partition input;
  • materialize/spool;
  • window function 的某些 frame/order;
  • exchange/shuffle;
  • blocking UDF/external call。

示例:

text
Pipeline A: Scan(adventurers) -> Filter -> HashBuild
                                      [breaker: hash table]
Pipeline B: Scan(quests) -> HashProbe -> AggregateBuild
                                      [breaker: group states]
Pipeline C: AggregateScan -> Sort -> Result
                              [breaker: sorted runs]

breaker 不一定把 data 写成通用 rows;中间状态可能是 hash table、run files、compressed vectors 或 partition buffers。

Fusing 的收益

  • 减少 materialized intermediate tuples;
  • 消除 operator dispatch;
  • column values 保持在 registers/local variables;
  • filter 后才 decode expensive output columns;
  • constants/types/NULLability 可 specialize;
  • compiler 可跨 expressions 做 common-subexpression elimination。

Fusing 的风险

  • giant function 增加 compile time/code size;
  • instruction cache pressure;
  • register pressure 导致 spills;
  • rare branches/UDF 让 code复杂;
  • skew/runtime cardinality 与 compile assumptions 偏离;
  • debugging/profiling/generated-code mapping 困难。

最优 pipeline 不是“能融合多少就融合多少”。engine 可在 operator/fragment 处切分,保持 code size 与 compile latency。

HyPer 的研究路线

HyPer 的 produce/consume code generation 将 relational operators 编译为 data-centric pipelines,核心目标是让 data 留在 CPU registers/cache,并避免 iterator overhead。它是 query compilation 研究的重要代表。

阅读入口:Thomas Neumann, Efficiently Compiling Efficient Query Plans for Modern Hardware(VLDB 2011)。论文结果建立在特定 prototype/workload/hardware 上,不能直接写成“任何 TPC-H 都比 PostgreSQL 快 10–100 倍”。

DuckDB 不应归类为“全查询 JIT”

DuckDB 使用 vectorized execution,Vector/DataChunk 是主要传输格式,当前 internals 文档描述 push-based vectorized physical execution。它可以通过 typed/vector kernels、constant/dictionary vectors 和 operator pipelines 获得低 overhead,但这不是 LLVM query JIT。

官方资料:

“C++ templates 在编译 DuckDB binary 时展开”也不等于 runtime 把每条 SQL 编译成专属 native function。

Adaptive/tiered compilation

系统可以先用 interpreter/vector engine立即执行,同时后台编译 hot pipeline,达到阈值后切换:

text
start quickly with generic code
  -> collect rows/types/selectivity/skew
  -> compile hot path
  -> switch at safe boundary

类似 JVM tiered compilation 的思想降低 cold-query latency,但实现要保证:

  • state 在 generic/compiled code 间 compatible;
  • snapshot/exception semantics 不变;
  • compiled assumptions 可 guard/deopt;
  • code cache 有 eviction;
  • concurrent compilation 有资源上限。

Compilation cache key

可复用 generated code 至少依赖:

  • logical/physical plan shape;
  • schema/type/nullability;
  • functions/operators/collation;
  • constants 是否 baked in;
  • CPU ISA/features;
  • security/tenant context;
  • engine version/config。

cache key 太具体命中低,太宽又可能错误复用。schema migration、extension/UDF replacement 和 CPU heterogeneous cluster 都需 invalidation/dispatch。

Parameter specialization

把 constant parameter 编进 code 可做 branch elimination,却为每个 value 生成 code;generic compiled function 复用高,但 optimization 少。

可选择:

  • generic parameters;
  • specialize hot values/shape;
  • runtime guard + fallback;
  • code cache size/TTL;
  • profile-guided recompile。

这与 optimizer generic/custom plan 问题相连:先选 plan,再决定 code specialization,两个层次都可能受 parameter skew 影响。

编译不改变 SQL 语义

generated code 必须保留:

  • three-valued NULL logic;
  • overflow/decimal precision;
  • collation/timezone;
  • floating NaN/order;
  • volatile function 调用次数与顺序约束;
  • exception/error timing允许的边界;
  • transaction snapshot 与 cancellation;
  • memory/resource accounting。

compiler 的常量折叠和重排不能使用 C/C++ 的普通算术语义替代数据库 type semantics。

Benchmark 框架

分开报告:

text
parse/bind/optimize time
code generation/optimization/emission time
first-row latency
steady-state execution CPU
total wall time
code cache hit/miss and bytes
rows/batches, branch/cache counters
peak memory and spill
result correctness

至少包含:short OLTP、medium repeated query、long CPU scan、I/O-bound query、complex UDF 和 parameter skew。只测 long scan 会夸大 compilation 收益。

验收清单

  • [ ] push/vectorized 与 JIT/codegen 分开描述;
  • [ ] pipeline breakers 按状态依赖识别;
  • [ ] compile time 计入端到端 latency;
  • [ ] code size/cache/invalidation 有边界;
  • [ ] SQL NULL/type/collation semantics 被保留;
  • [ ] 首次执行与复用执行分别测量;
  • [ ] DuckDB 不再被称为整查询 LLVM 编译器。

本章小结

查询编译从 expression JIT 逐步扩展到 pipeline code generation。它能减少 dispatch/materialization,却引入 compile latency、code-cache 与 specialization 风险。下一章回到另一条 CPU 优化路线:不为每行生成专属控制流,而是用 vector batches 和 SIMD 对同一类型数据批量处理。

Built with VitePress | Software Systems Atlas